mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-13 17:22:15 -05:00
Adding search, zoom, custom zoom and additional stylings to showplan (#18255)
* Lot of changes -Updating azdata to 0.0.13 -Updating prop views on node clicks -Context menu on graphs -Updating edge color on theme -Scrolling graph control like ssms -Zooming in, out and custom zoom on graph -Custom zoom widget -Node search widget * Fixing hygine errors * Code cleanup * Fixing action name * Renaming actions * equals dropdown * fixing tooltip * Code cleanup Fixing sorting function Adding functionality for replacement strings * Removing internal facing props from azdata proposed * Fixing hygine issue * Fixing web package hygiene * Updating yarn lock files * Fixing initial click
This commit is contained in:
@@ -75,7 +75,7 @@
|
||||
"angular2-grid": "2.0.6",
|
||||
"ansi_up": "^3.0.0",
|
||||
"applicationinsights": "1.0.8",
|
||||
"azdataGraph": "github:Microsoft/azdataGraph#0.0.11",
|
||||
"azdataGraph": "github:Microsoft/azdataGraph#0.0.14",
|
||||
"chart.js": "^2.9.4",
|
||||
"chokidar": "3.5.2",
|
||||
"graceful-fs": "4.2.6",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"applicationinsights": "1.0.8",
|
||||
"angular2-grid": "2.0.6",
|
||||
"ansi_up": "^3.0.0",
|
||||
"azdataGraph": "github:Microsoft/azdataGraph#0.0.11",
|
||||
"azdataGraph": "github:Microsoft/azdataGraph#0.0.14",
|
||||
"chart.js": "^2.9.4",
|
||||
"chokidar": "3.5.2",
|
||||
"cookie": "^0.4.0",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"@vscode/vscode-languagedetection": "1.0.18",
|
||||
"angular2-grid": "2.0.6",
|
||||
"ansi_up": "^3.0.0",
|
||||
"azdataGraph": "github:Microsoft/azdataGraph#0.0.11",
|
||||
"azdataGraph": "github:Microsoft/azdataGraph#0.0.14",
|
||||
"chart.js": "^2.9.4",
|
||||
"gridstack": "^3.1.3",
|
||||
"kburtram-query-plan": "2.6.1",
|
||||
|
||||
@@ -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.11":
|
||||
version "0.0.11"
|
||||
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/efeb59abc720c33e35386749e4345af028613672"
|
||||
"azdataGraph@github:Microsoft/azdataGraph#0.0.14":
|
||||
version "0.0.14"
|
||||
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/1fba9e94e5520ef78121f6dc23a5a2cdee20c8a4"
|
||||
|
||||
chalk@^2.3.0, chalk@^2.4.1:
|
||||
version "2.4.2"
|
||||
|
||||
@@ -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.11":
|
||||
version "0.0.11"
|
||||
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/efeb59abc720c33e35386749e4345af028613672"
|
||||
"azdataGraph@github:Microsoft/azdataGraph#0.0.14":
|
||||
version "0.0.14"
|
||||
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/1fba9e94e5520ef78121f6dc23a5a2cdee20c8a4"
|
||||
|
||||
binary-extensions@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
||||
8
src/sql/azdata.proposed.d.ts
vendored
8
src/sql/azdata.proposed.d.ts
vendored
@@ -1536,7 +1536,7 @@ declare module 'azdata' {
|
||||
/**
|
||||
* Flag to show/hide props in tooltip
|
||||
*/
|
||||
showInToolTip: boolean;
|
||||
showInTooltip: boolean;
|
||||
/**
|
||||
* Display order of property
|
||||
*/
|
||||
@@ -1544,7 +1544,11 @@ declare module 'azdata' {
|
||||
/**
|
||||
* Flag to indicate if the property has a longer value so that it will be shown at the bottom of the tooltip
|
||||
*/
|
||||
isLongString: boolean;
|
||||
positionAtBottom: boolean;
|
||||
/**
|
||||
* Display value of property to show in tooltip and other UI element.
|
||||
*/
|
||||
displayValue: string;
|
||||
}
|
||||
|
||||
export interface ExecutionPlanRecommendations {
|
||||
|
||||
@@ -74,7 +74,10 @@ export function endsWith(haystack: string, needle: string): boolean {
|
||||
|
||||
/**
|
||||
* Remove line breaks/eols from a string across different operating systems.
|
||||
* @param str target strings that needs line breaks removed.
|
||||
* @param replace optional string that replaces the line breaks.
|
||||
* @returns string with removed line breaks.
|
||||
*/
|
||||
export function removeLineBreaks(str: string): string {
|
||||
return str.replace(/(\r\n|\n|\r)/gm, '');
|
||||
export function removeLineBreaks(str: string, replace?: string): string {
|
||||
return str.replace(/(\r\n|\n|\r)/gm, replace ?? '');
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ However we always want it to be the width of the container it is resizing.
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
/* views created by the action-bar actions */
|
||||
@@ -51,22 +51,21 @@ However we always want it to be the width of the container it is resizing.
|
||||
}
|
||||
|
||||
/* Search node action view */
|
||||
.qps-container .query-plan .plan .plan-action-container .search-node-view {
|
||||
.qps-container .query-plan .plan .plan-action-container .search-node-widget {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border: 1px solid;
|
||||
padding: 5px;
|
||||
height: auto;
|
||||
width: 470px;
|
||||
}
|
||||
|
||||
/* input bar styling in search node action view */
|
||||
.qps-container .query-plan .plan .plan-action-container .search-node-view .search-bar-container{
|
||||
.qps-container .query-plan .plan .plan-action-container .search-node-widget .select-container{
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
/* styling for select element in search node action view */
|
||||
.qps-container .query-plan .plan .plan-action-container .search-node-view .search-bar-container > select{
|
||||
.qps-container .query-plan .plan .plan-action-container .search-node-widget .select-container > select{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -74,7 +73,6 @@ However we always want it to be the width of the container it is resizing.
|
||||
.qps-container .query-plan .plan .plan-action-container .custom-zoom-view {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border: 1px solid;
|
||||
padding: 5px;
|
||||
height: auto;
|
||||
width: 180px;
|
||||
@@ -82,11 +80,10 @@ However we always want it to be the width of the container it is resizing.
|
||||
|
||||
/* query plan header that contains the relative query cost, query statement and recommendations */
|
||||
.qps-container .query-plan .plan .header {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
border-bottom: solid 1px;
|
||||
padding: 5px;
|
||||
border-top: 1px solid;
|
||||
border-bottom: 1px solid;
|
||||
font-weight: bolder;
|
||||
padding-left: 5px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* each link in query plan recommendations */
|
||||
@@ -108,7 +105,7 @@ However we always want it to be the width of the container it is resizing.
|
||||
flex: 0 0 500px;
|
||||
overflow: hidden;
|
||||
width: 500px;
|
||||
height: 100%;
|
||||
height: calc( 100% - 2px );
|
||||
border: 1px solid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -213,3 +210,13 @@ However we always want it to be the width of the container it is resizing.
|
||||
.qps-container .properties-toggle .collapse {
|
||||
background: url(../images/collapse.gif) no-repeat center center;
|
||||
}
|
||||
|
||||
/* Stylings necessary for tooltips to show up next to target nodes*/
|
||||
.mxTooltip {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
position: absolute;
|
||||
cursor: default;
|
||||
padding: 4px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export class PlanHeader {
|
||||
this.renderGraphIndexAndCost();
|
||||
}
|
||||
public set query(query: string) {
|
||||
this._query = removeLineBreaks(query);
|
||||
this._query = removeLineBreaks(query, ' ');
|
||||
this.renderQueryText();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,15 +16,15 @@ import * as azdataGraphModule from 'azdataGraph';
|
||||
import { queryPlanNodeIconPaths } from 'sql/workbench/contrib/queryplan2/browser/constants';
|
||||
import { isString } from 'vs/base/common/types';
|
||||
import { PlanHeader } from 'sql/workbench/contrib/queryplan2/browser/planHeader';
|
||||
import { GraphElementPropertiesView } from 'sql/workbench/contrib/queryplan2/browser/graphElementPropertiesView';
|
||||
import { QueryPlanPropertiesView } from 'sql/workbench/contrib/queryplan2/browser/queryPlanPropertiesView';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { openNewQuery } from 'sql/workbench/contrib/query/browser/queryActions';
|
||||
import { RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { editorBackground, foreground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { editorBackground, editorWidgetBackground, foreground, textLinkForeground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { ISashEvent, ISashLayoutProvider, Orientation, Sash } from 'vs/base/browser/ui/sash/sash';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
@@ -32,11 +32,29 @@ import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/commo
|
||||
import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
|
||||
import { Progress } from 'vs/platform/progress/common/progress';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions';
|
||||
import { QueryPlanWidgetController } from 'sql/workbench/contrib/queryplan2/browser/queryPlanWidgetController';
|
||||
import { CustomZoomWidget } from 'sql/workbench/contrib/queryplan2/browser/widgets/customZoomWidget';
|
||||
import { NodeSearchWidget } from 'sql/workbench/contrib/queryplan2/browser/widgets/nodeSearchWidget';
|
||||
|
||||
let azdataGraph = azdataGraphModule();
|
||||
|
||||
export interface InternalExecutionPlanNode extends azdata.ExecutionPlanNode {
|
||||
/**
|
||||
* Unique internal id given to graph node by ADS.
|
||||
*/
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface InternalExecutionPlanEdge extends azdata.ExecutionPlanEdge {
|
||||
/**
|
||||
* Unique internal id given to graph edge by ADS.
|
||||
*/
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export class QueryPlan2Tab implements IPanelTab {
|
||||
public readonly title = localize('queryPlanTitle', "Query Plan");
|
||||
public readonly title = localize('queryPlanTitle', "Query Plan (Preview)");
|
||||
public readonly identifier = 'QueryPlan2Tab';
|
||||
public readonly view: QueryPlan2View;
|
||||
|
||||
@@ -92,7 +110,7 @@ export class QueryPlan2View implements IPanelView {
|
||||
if (newGraphs) {
|
||||
newGraphs.forEach(g => {
|
||||
const qp2 = this.instantiationService.createInstance(QueryPlan2, this._container, this._qps.length + 1);
|
||||
qp2.graph = g;
|
||||
qp2.graphModel = g;
|
||||
this._qps.push(qp2);
|
||||
this._graphs.push(g);
|
||||
this.updateRelativeCosts();
|
||||
@@ -107,14 +125,14 @@ export class QueryPlan2View implements IPanelView {
|
||||
|
||||
if (sum > 0) {
|
||||
this._qps.forEach(qp => {
|
||||
qp.planHeader.relativeCost = ((qp.graph.root.subTreeCost + qp.graph.root.cost) / sum) * 100;
|
||||
qp.planHeader.relativeCost = ((qp.graphModel.root.subTreeCost + qp.graphModel.root.cost) / sum) * 100;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class QueryPlan2 implements ISashLayoutProvider {
|
||||
private _graph?: azdata.ExecutionPlanGraph;
|
||||
private _graphModel?: azdata.ExecutionPlanGraph;
|
||||
|
||||
private _container: HTMLElement;
|
||||
|
||||
@@ -125,25 +143,33 @@ export class QueryPlan2 implements ISashLayoutProvider {
|
||||
private _planContainer: HTMLElement;
|
||||
private _planHeaderContainer: HTMLElement;
|
||||
|
||||
public propertiesView: GraphElementPropertiesView;
|
||||
public propertiesView: QueryPlanPropertiesView;
|
||||
private _propContainer: HTMLElement;
|
||||
|
||||
private _azdataGraphDiagram: any;
|
||||
private _planActionContainer: HTMLElement;
|
||||
public planActionView: QueryPlanWidgetController;
|
||||
|
||||
public azdataGraphDiagram: any;
|
||||
|
||||
public graphElementPropertiesSet: Set<string> = new Set();
|
||||
|
||||
private uniqueElementId: number = -1;
|
||||
|
||||
constructor(
|
||||
parent: HTMLElement,
|
||||
private _parent: HTMLElement,
|
||||
private _graphIndex: number,
|
||||
@IInstantiationService public readonly _instantiationService: IInstantiationService,
|
||||
@IThemeService private readonly _themeService: IThemeService,
|
||||
@IContextViewService public readonly contextViewService: IContextViewService,
|
||||
@IUntitledTextEditorService private readonly _untitledEditorService: IUntitledTextEditorService,
|
||||
@IEditorService private readonly editorService: IEditorService
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IContextMenuService private _contextMenuService: IContextMenuService,
|
||||
) {
|
||||
// parent container for query plan.
|
||||
this._container = DOM.$('.query-plan');
|
||||
parent.appendChild(this._container);
|
||||
this._parent.appendChild(this._container);
|
||||
const sashContainer = DOM.$('.query-plan-sash');
|
||||
parent.appendChild(sashContainer);
|
||||
this._parent.appendChild(sashContainer);
|
||||
|
||||
const sash = new Sash(sashContainer, this, { orientation: Orientation.HORIZONTAL });
|
||||
let originalHeight = this._container.offsetHeight;
|
||||
@@ -178,6 +204,12 @@ export class QueryPlan2 implements ISashLayoutProvider {
|
||||
|
||||
// container that holds plan header info
|
||||
this._planHeaderContainer = DOM.$('.header');
|
||||
|
||||
// Styling header text like the query editor
|
||||
this._planHeaderContainer.style.fontFamily = EDITOR_FONT_DEFAULTS.fontFamily;
|
||||
this._planHeaderContainer.style.fontSize = EDITOR_FONT_DEFAULTS.fontSize.toString();
|
||||
this._planHeaderContainer.style.fontWeight = EDITOR_FONT_DEFAULTS.fontWeight;
|
||||
|
||||
this._planContainer.appendChild(this._planHeaderContainer);
|
||||
this.planHeader = this._instantiationService.createInstance(PlanHeader, this._planHeaderContainer, {
|
||||
planIndex: this._graphIndex,
|
||||
@@ -186,7 +218,11 @@ export class QueryPlan2 implements ISashLayoutProvider {
|
||||
// container properties
|
||||
this._propContainer = DOM.$('.properties');
|
||||
this._container.appendChild(this._propContainer);
|
||||
this.propertiesView = new GraphElementPropertiesView(this._propContainer, this._themeService);
|
||||
this.propertiesView = new QueryPlanPropertiesView(this._propContainer, this._themeService);
|
||||
|
||||
this._planActionContainer = DOM.$('.plan-action-container');
|
||||
this._planContainer.appendChild(this._planActionContainer);
|
||||
this.planActionView = new QueryPlanWidgetController(this._planActionContainer);
|
||||
|
||||
// container that holds actionbar icons
|
||||
this._actionBarContainer = DOM.$('.action-bar-container');
|
||||
@@ -197,7 +233,7 @@ export class QueryPlan2 implements ISashLayoutProvider {
|
||||
|
||||
|
||||
const actions = [
|
||||
new SaveXml(),
|
||||
new SavePlanFile(),
|
||||
new OpenGraphFile(),
|
||||
new OpenQueryAction(),
|
||||
new SearchNodeAction(),
|
||||
@@ -209,7 +245,22 @@ export class QueryPlan2 implements ISashLayoutProvider {
|
||||
];
|
||||
this._actionBar.pushAction(actions, { icon: true, label: false });
|
||||
|
||||
|
||||
// Setting up context menu
|
||||
const self = this;
|
||||
this._container.oncontextmenu = (e: MouseEvent) => {
|
||||
if (actions) {
|
||||
this._contextMenuService.showContextMenu({
|
||||
getAnchor: () => {
|
||||
return {
|
||||
x: e.x,
|
||||
y: e.y
|
||||
};
|
||||
},
|
||||
getActions: () => actions,
|
||||
getActionsContext: () => (self)
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getHorizontalSashTop(sash: Sash): number {
|
||||
@@ -222,8 +273,11 @@ export class QueryPlan2 implements ISashLayoutProvider {
|
||||
return this._container.clientWidth;
|
||||
}
|
||||
|
||||
private populate(node: azdata.ExecutionPlanNode, diagramNode: any): any {
|
||||
private populate(node: InternalExecutionPlanNode, diagramNode: any): any {
|
||||
diagramNode.label = node.name;
|
||||
const nodeId = this.createGraphElementId();
|
||||
diagramNode.id = nodeId;
|
||||
node.id = nodeId;
|
||||
|
||||
if (node.properties && node.properties.length > 0) {
|
||||
diagramNode.metrics = this.populateProperties(node.properties);
|
||||
@@ -233,6 +287,13 @@ export class QueryPlan2 implements ISashLayoutProvider {
|
||||
diagramNode.icon = node.type;
|
||||
}
|
||||
|
||||
if (node.edges) {
|
||||
diagramNode.edges = [];
|
||||
for (let i = 0; i < node.edges.length; i++) {
|
||||
diagramNode.edges.push(this.populateEdges(node.edges[i], new Object()));
|
||||
}
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
diagramNode.children = [];
|
||||
for (let i = 0; i < node.children.length; ++i) {
|
||||
@@ -240,55 +301,80 @@ export class QueryPlan2 implements ISashLayoutProvider {
|
||||
}
|
||||
}
|
||||
|
||||
if (node.edges) {
|
||||
diagramNode.edges = [];
|
||||
for (let i = 0; i < node.edges.length; i++) {
|
||||
diagramNode.edges.push(this.populateEdges(node.edges[i], new Object()));
|
||||
}
|
||||
if (node.description) {
|
||||
diagramNode.description = node.description;
|
||||
}
|
||||
return diagramNode;
|
||||
}
|
||||
|
||||
private populateEdges(edge: azdata.ExecutionPlanEdge, diagramEdge: any) {
|
||||
private populateEdges(edge: InternalExecutionPlanEdge, diagramEdge: any) {
|
||||
diagramEdge.label = '';
|
||||
const edgeId = this.createGraphElementId();
|
||||
diagramEdge.id = edgeId;
|
||||
edge.id = edgeId;
|
||||
diagramEdge.metrics = this.populateProperties(edge.properties);
|
||||
diagramEdge.weight = Math.max(0.5, Math.min(0.5 + 0.75 * Math.log10(edge.rowCount), 6));
|
||||
return diagramEdge;
|
||||
}
|
||||
|
||||
private populateProperties(props: azdata.ExecutionPlanGraphElementProperty[]) {
|
||||
return props.filter(e => isString(e.value))
|
||||
return props.filter(e => isString(e.displayValue) && e.showInTooltip)
|
||||
.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||
.map(e => {
|
||||
this.graphElementPropertiesSet.add(e.name);
|
||||
return {
|
||||
name: e.name,
|
||||
value: e.value.toString().substring(0, 75)
|
||||
value: e.displayValue,
|
||||
isLongString: e.positionAtBottom
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private createPlanDiagram(container: HTMLElement): void {
|
||||
private createGraphElementId(): string {
|
||||
this.uniqueElementId += 1;
|
||||
return `element-${this.uniqueElementId}`;
|
||||
}
|
||||
|
||||
private createPlanDiagram(container: HTMLElement) {
|
||||
let diagramRoot: any = new Object();
|
||||
let graphRoot: azdata.ExecutionPlanNode = this._graph.root;
|
||||
let graphRoot: azdata.ExecutionPlanNode = this._graphModel.root;
|
||||
|
||||
this.populate(graphRoot, diagramRoot);
|
||||
this._azdataGraphDiagram = new azdataGraph.azdataQueryPlan(container, diagramRoot, queryPlanNodeIconPaths);
|
||||
this.azdataGraphDiagram = new azdataGraph.azdataQueryPlan(container, diagramRoot, queryPlanNodeIconPaths);
|
||||
|
||||
this.azdataGraphDiagram.graph.addListener('click', (sender, evt) => {
|
||||
// Updating properties view table on node clicks
|
||||
const cell = evt.properties['cell'];
|
||||
if (cell) {
|
||||
this.propertiesView.graphElement = this.searchNodes(cell.id);
|
||||
} else if (!this.azdataGraphDiagram.graph.getSelectionCell()) {
|
||||
const root = this.azdataGraphDiagram.graph.model.getCell(diagramRoot.id);
|
||||
this.azdataGraphDiagram.graph.getSelectionModel().setCell(root);
|
||||
this.propertiesView.graphElement = this.searchNodes(diagramRoot.id);
|
||||
evt.consume();
|
||||
} else {
|
||||
evt.consume();
|
||||
}
|
||||
});
|
||||
|
||||
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
|
||||
const iconBackground = theme.getColor(editorBackground);
|
||||
if (iconBackground) {
|
||||
this._azdataGraphDiagram.setIconBackgroundColor(iconBackground);
|
||||
this.azdataGraphDiagram.setIconBackgroundColor(iconBackground);
|
||||
}
|
||||
|
||||
const iconLabelColor = theme.getColor(foreground);
|
||||
if (iconLabelColor) {
|
||||
this._azdataGraphDiagram.setTextFontColor(iconLabelColor);
|
||||
this.azdataGraphDiagram.setTextFontColor(iconLabelColor);
|
||||
this.azdataGraphDiagram.setEdgeColor(iconLabelColor);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public set graph(graph: azdata.ExecutionPlanGraph | undefined) {
|
||||
this._graph = graph;
|
||||
if (this._graph) {
|
||||
public set graphModel(graph: azdata.ExecutionPlanGraph | undefined) {
|
||||
this._graphModel = graph;
|
||||
if (this._graphModel) {
|
||||
this.planHeader.graphIndex = this._graphIndex;
|
||||
this.planHeader.query = graph.query;
|
||||
if (graph.recommendations) {
|
||||
@@ -296,27 +382,59 @@ export class QueryPlan2 implements ISashLayoutProvider {
|
||||
}
|
||||
let diagramContainer = DOM.$('.diagram');
|
||||
this.createPlanDiagram(diagramContainer);
|
||||
|
||||
/**
|
||||
* We do not want to scroll the diagram through mouse wheel.
|
||||
* Instead, we pass this event to parent control. So, when user
|
||||
* uses the scroll wheel, they scroll through graphs present in
|
||||
* the graph control. To scroll the individual graphs, users should
|
||||
* use the scroll bars.
|
||||
*/
|
||||
diagramContainer.addEventListener('wheel', e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._parent.scrollTop += e.deltaY;
|
||||
});
|
||||
|
||||
this._planContainer.appendChild(diagramContainer);
|
||||
|
||||
this.propertiesView.graphElement = this._graph.root;
|
||||
this.propertiesView.graphElement = this._graphModel.root;
|
||||
}
|
||||
}
|
||||
|
||||
public get graph(): azdata.ExecutionPlanGraph | undefined {
|
||||
return this._graph;
|
||||
public get graphModel(): azdata.ExecutionPlanGraph | undefined {
|
||||
return this._graphModel;
|
||||
}
|
||||
|
||||
public openQuery() {
|
||||
return this._instantiationService.invokeFunction(openNewQuery, undefined, this.graph.query, RunQueryOnConnectionMode.none).then();
|
||||
return this._instantiationService.invokeFunction(openNewQuery, undefined, this.graphModel.query, RunQueryOnConnectionMode.none).then();
|
||||
}
|
||||
|
||||
public async openGraphFile() {
|
||||
const input = this._untitledEditorService.create({ mode: this.graph.graphFile.graphFileType, initialValue: this.graph.graphFile.graphFileContent });
|
||||
const input = this._untitledEditorService.create({ mode: this.graphModel.graphFile.graphFileType, initialValue: this.graphModel.graphFile.graphFileContent });
|
||||
await input.resolve();
|
||||
await this._instantiationService.invokeFunction(formatDocumentWithSelectedProvider, input.textEditorModel, FormattingMode.Explicit, Progress.None, CancellationToken.None);
|
||||
input.setDirty(false);
|
||||
this.editorService.openEditor(input);
|
||||
}
|
||||
|
||||
|
||||
public searchNodes(searchId: string): InternalExecutionPlanNode | InternalExecutionPlanEdge | undefined {
|
||||
let stack: InternalExecutionPlanNode[] = [];
|
||||
stack.push(this._graphModel.root);
|
||||
while (stack.length !== 0) {
|
||||
const currentNode = stack.pop();
|
||||
if (currentNode.id === searchId) {
|
||||
return currentNode;
|
||||
}
|
||||
stack.push(...currentNode.children);
|
||||
const resultEdge = currentNode.edges.find(e => (<InternalExecutionPlanEdge>e).id === searchId);
|
||||
if (resultEdge) {
|
||||
return resultEdge;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class OpenQueryAction extends Action {
|
||||
@@ -354,6 +472,7 @@ class ZoomInAction extends Action {
|
||||
}
|
||||
|
||||
public override async run(context: QueryPlan2): Promise<void> {
|
||||
context.azdataGraphDiagram.graph.zoomIn();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,6 +485,7 @@ class ZoomOutAction extends Action {
|
||||
}
|
||||
|
||||
public override async run(context: QueryPlan2): Promise<void> {
|
||||
context.azdataGraphDiagram.graph.zoomOut();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,22 +498,24 @@ class ZoomToFitAction extends Action {
|
||||
}
|
||||
|
||||
public override async run(context: QueryPlan2): Promise<void> {
|
||||
context.azdataGraphDiagram.graph.fit();
|
||||
context.azdataGraphDiagram.graph.view.rendering = true;
|
||||
context.azdataGraphDiagram.graph.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
class SaveXml extends Action {
|
||||
class SavePlanFile extends Action {
|
||||
public static ID = 'qp.saveXML';
|
||||
public static LABEL = localize('queryPlanSavePlanXML', "Save XML");
|
||||
public static LABEL = localize('queryPlanSavePlanXML', "Save Plan File");
|
||||
|
||||
constructor() {
|
||||
super(SaveXml.ID, SaveXml.LABEL, Codicon.save.classNames);
|
||||
super(SavePlanFile.ID, SavePlanFile.LABEL, Codicon.save.classNames);
|
||||
}
|
||||
|
||||
public override async run(context: QueryPlan2): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class CustomZoomAction extends Action {
|
||||
public static ID = 'qp.customZoom';
|
||||
public static LABEL = localize('queryPlanCustomZoom', "Custom Zoom");
|
||||
@@ -403,6 +525,7 @@ class CustomZoomAction extends Action {
|
||||
}
|
||||
|
||||
public override async run(context: QueryPlan2): Promise<void> {
|
||||
context.planActionView.toggleWidget(context._instantiationService.createInstance(CustomZoomWidget, context));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,6 +538,7 @@ class SearchNodeAction extends Action {
|
||||
}
|
||||
|
||||
public override async run(context: QueryPlan2): Promise<void> {
|
||||
context.planActionView.toggleWidget(context._instantiationService.createInstance(NodeSearchWidget, context));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,6 +572,27 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) =
|
||||
}
|
||||
`);
|
||||
}
|
||||
const shadow = theme.getColor(widgetShadow);
|
||||
const widgetBackgroundColor = theme.getColor(editorWidgetBackground);
|
||||
|
||||
if (shadow) {
|
||||
collector.addRule(`
|
||||
.qps-container .query-plan .plan .plan-action-container .child {
|
||||
box-shadow: 0 0 8px 2px ${shadow};
|
||||
background-color: ${widgetBackgroundColor};
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
const textColor = theme.getColor(foreground);
|
||||
if (widgetBackgroundColor && foreground) {
|
||||
collector.addRule(`
|
||||
.mxTooltip {
|
||||
color: ${textColor};
|
||||
background-color: ${widgetBackgroundColor};
|
||||
}
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -13,12 +13,13 @@ import { attachTableStyler } from 'sql/platform/theme/common/styler';
|
||||
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
|
||||
import { Table } from 'sql/base/browser/ui/table/table';
|
||||
import { RESULTS_GRID_DEFAULTS } from 'sql/workbench/common/constants';
|
||||
import { isString } from 'vs/base/common/types';
|
||||
import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
|
||||
import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { removeLineBreaks } from 'sql/base/common/strings';
|
||||
import { isString } from 'vs/base/common/types';
|
||||
|
||||
|
||||
export class GraphElementPropertiesView {
|
||||
export class QueryPlanPropertiesView {
|
||||
|
||||
// Title bar with close button action
|
||||
private _propertiesTitle!: HTMLElement;
|
||||
@@ -178,7 +179,7 @@ export class GraphElementPropertiesView {
|
||||
private renderView(): void {
|
||||
if (this._model.graphElement) {
|
||||
const nodeName = (<azdata.ExecutionPlanNode>this._model.graphElement).name;
|
||||
this._operationName.innerText = nodeName ?? localize('queryPlanPropertiesEdgeOperationName', "Edge"); //since edges do not have names like node, we set the operation name to 'Edge'
|
||||
this._operationName.innerText = nodeName ? removeLineBreaks(nodeName) : localize('queryPlanPropertiesEdgeOperationName', "Edge"); //since edges do not have names like node, we set the operation name to 'Edge'
|
||||
}
|
||||
this._tableContainer.scrollTo(0, 0);
|
||||
this._dataView.clear();
|
||||
@@ -199,13 +200,13 @@ export class GraphElementPropertiesView {
|
||||
let row = {};
|
||||
row['name'] = '\t'.repeat(indent) + p.name;
|
||||
row['parent'] = parentIndex;
|
||||
rows.push(row);
|
||||
if (!isString(p.value)) {
|
||||
row['value'] = '';
|
||||
this.convertPropertiesToTableRows(p.value, rows.length - 1, indent + 2, rows);
|
||||
} else {
|
||||
row['value'] = p.value;
|
||||
}
|
||||
rows.push(row);
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
@@ -228,7 +229,7 @@ export class ClosePropertyViewAction extends Action {
|
||||
super(ClosePropertyViewAction.ID, ClosePropertyViewAction.LABEL, Codicon.close.classNames);
|
||||
}
|
||||
|
||||
public override async run(context: GraphElementPropertiesView): Promise<void> {
|
||||
public override async run(context: QueryPlanPropertiesView): Promise<void> {
|
||||
context.toggleVisibility();
|
||||
}
|
||||
}
|
||||
@@ -241,7 +242,7 @@ export class SortPropertiesAlphabeticallyAction extends Action {
|
||||
super(SortPropertiesAlphabeticallyAction.ID, SortPropertiesAlphabeticallyAction.LABEL, Codicon.sortPrecedence.classNames);
|
||||
}
|
||||
|
||||
public override async run(context: GraphElementPropertiesView): Promise<void> {
|
||||
public override async run(context: QueryPlanPropertiesView): Promise<void> {
|
||||
context.sortPropertiesAlphabetically();
|
||||
}
|
||||
}
|
||||
@@ -254,7 +255,7 @@ export class SortPropertiesByDisplayOrderAction extends Action {
|
||||
super(SortPropertiesByDisplayOrderAction.ID, SortPropertiesByDisplayOrderAction.LABEL, Codicon.listOrdered.classNames);
|
||||
}
|
||||
|
||||
public override async run(context: GraphElementPropertiesView): Promise<void> {
|
||||
public override async run(context: QueryPlanPropertiesView): Promise<void> {
|
||||
context.sortPropertiesByImportance();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export abstract class QueryPlanWidgetBase {
|
||||
/**
|
||||
*
|
||||
* @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) {
|
||||
this.container = container;
|
||||
this.identifier = identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called when the view is added to PlanActionView.
|
||||
* Generally, the view should focus the first input element in the view
|
||||
*/
|
||||
public abstract focus();
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { QueryPlanWidgetBase } from 'sql/workbench/contrib/queryplan2/browser/queryPlanWidgetBase';
|
||||
|
||||
export class QueryPlanWidgetController {
|
||||
private _queryPlanWidgetMap: Map<string, QueryPlanWidgetBase> = new Map();
|
||||
|
||||
constructor(private _parentContainer: HTMLElement) {
|
||||
|
||||
}
|
||||
|
||||
private addWidget(widget: QueryPlanWidgetBase) {
|
||||
if (widget.identifier && !this._queryPlanWidgetMap.has(widget.identifier)) {
|
||||
this._queryPlanWidgetMap.set(widget.identifier, widget);
|
||||
if (widget.container) {
|
||||
widget.container.classList.add('child');
|
||||
this._parentContainer.appendChild(widget.container);
|
||||
widget.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public removeWidget(widget: QueryPlanWidgetBase) {
|
||||
if (widget.identifier) {
|
||||
if (this._queryPlanWidgetMap.has(widget.identifier)) {
|
||||
this._parentContainer.removeChild(this._queryPlanWidgetMap.get(widget.identifier).container);
|
||||
this._queryPlanWidgetMap.delete(widget.identifier);
|
||||
} else {
|
||||
throw new Error('The view is not present in the container');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or removes view from the controller.
|
||||
* @param widget PlanActionView to be added.
|
||||
*/
|
||||
public toggleWidget(widget: QueryPlanWidgetBase) {
|
||||
if (!this._queryPlanWidgetMap.has(widget.identifier)) {
|
||||
this.addWidget(widget);
|
||||
} else {
|
||||
this.removeWidget(widget);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
|
||||
import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
|
||||
import { attachInputBoxStyler } from 'sql/platform/theme/common/styler';
|
||||
import { QueryPlanWidgetBase } from 'sql/workbench/contrib/queryplan2/browser/queryPlanWidgetBase';
|
||||
import { QueryPlan2 } from 'sql/workbench/contrib/queryplan2/browser/queryPlan';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
export class CustomZoomWidget extends QueryPlanWidgetBase {
|
||||
private _actionBar: ActionBar;
|
||||
public customZoomInputBox: InputBox;
|
||||
|
||||
constructor(
|
||||
public readonly queryPlanView: QueryPlan2,
|
||||
@IContextViewService public readonly contextViewService: IContextViewService,
|
||||
@IThemeService public readonly themeService: IThemeService,
|
||||
@INotificationService public readonly notificationService: INotificationService
|
||||
) {
|
||||
super(DOM.$('.custom-zoom-view'), 'customZoom');
|
||||
|
||||
// Custom zoom input box
|
||||
const zoomValueLabel = localize("qpZoomValueLabel", 'Zoom (percent)');
|
||||
this.customZoomInputBox = new InputBox(this.container, this.contextViewService, {
|
||||
type: 'number',
|
||||
ariaLabel: zoomValueLabel,
|
||||
flexibleWidth: false
|
||||
});
|
||||
attachInputBoxStyler(this.customZoomInputBox, this.themeService);
|
||||
|
||||
const currentZoom = queryPlanView.azdataGraphDiagram.graph.view.getScale();
|
||||
|
||||
// Setting initial value to graph's current zoom
|
||||
this.customZoomInputBox.value = Math.round(currentZoom).toString();
|
||||
|
||||
// Setting up keyboard shortcuts
|
||||
const self = this;
|
||||
this.customZoomInputBox.element.onkeydown = async (ev) => {
|
||||
if (ev.key === 'Enter') {
|
||||
await new CustomZoomAction().run(self);
|
||||
} else if (ev.key === 'Escape') {
|
||||
queryPlanView.planActionView.removeWidget(self);
|
||||
}
|
||||
};
|
||||
|
||||
// Adding action bar
|
||||
this._actionBar = new ActionBar(this.container);
|
||||
this._actionBar.context = this;
|
||||
this._actionBar.pushAction(new CustomZoomAction(), { label: false, icon: true });
|
||||
this._actionBar.pushAction(new CancelZoom(), { label: false, icon: true });
|
||||
}
|
||||
|
||||
// Setting initial focus to input box
|
||||
public focus() {
|
||||
this.customZoomInputBox.focus();
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomZoomAction extends Action {
|
||||
public static ID = 'qp.customZoomAction';
|
||||
public static LABEL = localize('zoomAction', "Zoom (Enter)");
|
||||
|
||||
constructor() {
|
||||
super(CustomZoomAction.ID, CustomZoomAction.LABEL, Codicon.zoomOut.classNames);
|
||||
}
|
||||
|
||||
public override async run(context: CustomZoomWidget): Promise<void> {
|
||||
const newValue = parseInt(context.customZoomInputBox.value);
|
||||
if (newValue <= 200 && newValue >= 1) { // Getting max and min zoom values from SSMS
|
||||
context.queryPlanView.azdataGraphDiagram.graph.view.setScale(newValue / 100);
|
||||
context.queryPlanView.planActionView.removeWidget(context);
|
||||
} else {
|
||||
context.notificationService.error(
|
||||
localize('invalidCustomZoomError', "Select a zoom value between 1 to 200")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CancelZoom extends Action {
|
||||
public static ID = 'qp.cancelCustomZoomAction';
|
||||
public static LABEL = localize('cancelCustomZoomAction', "Close (Escape)");
|
||||
|
||||
constructor() {
|
||||
super(CancelZoom.ID, CancelZoom.LABEL, Codicon.chromeClose.classNames);
|
||||
}
|
||||
|
||||
public override async run(context: CustomZoomWidget): Promise<void> {
|
||||
context.queryPlanView.planActionView.removeWidget(context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { QueryPlanWidgetBase } from 'sql/workbench/contrib/queryplan2/browser/queryPlanWidgetBase';
|
||||
import { ActionBar } from 'sql/base/browser/ui/taskbar/actionbar';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { InternalExecutionPlanNode, QueryPlan2 } from 'sql/workbench/contrib/queryplan2/browser/queryPlan';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { attachInputBoxStyler, 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 { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
|
||||
import { isString } from 'vs/base/common/types';
|
||||
|
||||
const CONTAINS_DISPLAY_STRING = localize("queryPlanSearchTypeContains", 'Contains');
|
||||
const EQUALS_DISPLAY_STRING = localize("queryPlanSearchTypeEquals", 'Equals');
|
||||
|
||||
export class NodeSearchWidget extends QueryPlanWidgetBase {
|
||||
|
||||
private _propertyNameSelectBoxContainer: HTMLElement;
|
||||
private _propertyNameSelectBox: SelectBox;
|
||||
|
||||
private _searchTypeSelectBoxContainer: HTMLElement;
|
||||
private _searchTypeSelectBox: SelectBox;
|
||||
|
||||
private _searchTextInputBox: InputBox;
|
||||
private _searchResults: string[] = [];
|
||||
private _currentSearchResultIndex = 0;
|
||||
private _usePreviousSearchResult: boolean = false;
|
||||
|
||||
private _actionBar: ActionBar;
|
||||
|
||||
constructor(
|
||||
public readonly queryPlanView: QueryPlan2,
|
||||
@IContextViewService public readonly contextViewService: IContextViewService,
|
||||
@IThemeService public readonly themeService: IThemeService
|
||||
|
||||
) {
|
||||
super(DOM.$('.search-node-widget'), 'searchWidget');
|
||||
|
||||
// property name dropdown
|
||||
this._propertyNameSelectBoxContainer = DOM.$('.search-widget-property-name-select-box .dropdown-container');
|
||||
this.container.appendChild(this._propertyNameSelectBoxContainer);
|
||||
const propDropdownOptions = [...queryPlanView.graphElementPropertiesSet].sort();
|
||||
this._propertyNameSelectBox = new SelectBox(propDropdownOptions, propDropdownOptions[0], this.contextViewService, this._propertyNameSelectBoxContainer);
|
||||
attachSelectBoxStyler(this._propertyNameSelectBox, this.themeService);
|
||||
this._propertyNameSelectBoxContainer.style.width = '150px';
|
||||
this._propertyNameSelectBox.render(this._propertyNameSelectBoxContainer);
|
||||
this._propertyNameSelectBox.onDidSelect(e => {
|
||||
this._usePreviousSearchResult = false;
|
||||
});
|
||||
|
||||
// search type dropdown
|
||||
this._searchTypeSelectBoxContainer = DOM.$('.search-widget-search-type-select-box .dropdown-container');
|
||||
this.container.appendChild(this._searchTypeSelectBoxContainer);
|
||||
this._searchTypeSelectBox = new SelectBox([
|
||||
EQUALS_DISPLAY_STRING,
|
||||
CONTAINS_DISPLAY_STRING
|
||||
], EQUALS_DISPLAY_STRING, this.contextViewService, this._searchTypeSelectBoxContainer);
|
||||
this._searchTypeSelectBox.render(this._searchTypeSelectBoxContainer);
|
||||
attachSelectBoxStyler(this._searchTypeSelectBox, this.themeService);
|
||||
this._searchTypeSelectBoxContainer.style.width = '100px';
|
||||
this._searchTypeSelectBox.onDidSelect(e => {
|
||||
this._usePreviousSearchResult = false;
|
||||
});
|
||||
|
||||
// search text input box
|
||||
this._searchTextInputBox = new InputBox(this.container, this.contextViewService, {});
|
||||
attachInputBoxStyler(this._searchTextInputBox, this.themeService);
|
||||
this._searchTextInputBox.element.style.marginLeft = '5px';
|
||||
this._searchTextInputBox.onDidChange(e => {
|
||||
this._usePreviousSearchResult = false;
|
||||
});
|
||||
|
||||
|
||||
// setting up key board shortcuts
|
||||
const self = this;
|
||||
this._searchTextInputBox.element.onkeydown = async e => {
|
||||
if (e.key === 'Enter' && e.shiftKey) {
|
||||
await new GoToPreviousMatchAction().run(self);
|
||||
} else if (e.key === 'Enter') {
|
||||
await new GoToNextMatchAction().run(self);
|
||||
} else if (e.key === 'Escape') {
|
||||
await new CancelSearch().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 });
|
||||
}
|
||||
|
||||
// Initial focus is set to the search text input box
|
||||
public focus() {
|
||||
this._searchTextInputBox.focus();
|
||||
}
|
||||
|
||||
|
||||
public searchNode(returnPreviousResult: boolean): void {
|
||||
|
||||
// Searching again as the input params have changed
|
||||
if (!this._usePreviousSearchResult) {
|
||||
|
||||
this._searchResults = [];
|
||||
this._currentSearchResultIndex = 0; //Resetting search Index to 0;
|
||||
this._usePreviousSearchResult = true;
|
||||
|
||||
// Doing depth first search in the graphModel to find nodes with matching prop values.
|
||||
const graphModel = this.queryPlanView.graphModel;
|
||||
const stack: InternalExecutionPlanNode[] = [];
|
||||
stack.push(graphModel.root);
|
||||
|
||||
while (stack.length !== 0) {
|
||||
const currentNode = stack.pop();
|
||||
|
||||
const matchingProp = currentNode.properties.find(e => e.name === this._propertyNameSelectBox.value);
|
||||
|
||||
// Searching only properties with string value.
|
||||
if (isString(matchingProp?.value)) {
|
||||
// If the search type is '=' we look for exact match and for 'contains' we look search string occurance in prop value
|
||||
if (
|
||||
this._searchTypeSelectBox.value === EQUALS_DISPLAY_STRING && matchingProp.value === this._searchTextInputBox.value ||
|
||||
this._searchTypeSelectBox.value === CONTAINS_DISPLAY_STRING && matchingProp.value.includes(this._searchTextInputBox.value)
|
||||
) {
|
||||
this._searchResults.push(currentNode.id);
|
||||
}
|
||||
}
|
||||
|
||||
stack.push(...currentNode.children);
|
||||
}
|
||||
}
|
||||
// Returning if no results found.
|
||||
if (this._searchResults.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Getting the node at search index
|
||||
const resultCell = this.queryPlanView.azdataGraphDiagram.graph.model.getCell(this._searchResults[this._currentSearchResultIndex]);
|
||||
// Selecting the node on graph diagram
|
||||
this.queryPlanView.azdataGraphDiagram.graph.setSelectionCell(resultCell);
|
||||
this.queryPlanView.propertiesView.graphElement = this.queryPlanView.searchNodes(resultCell.id);
|
||||
|
||||
/**
|
||||
* The selected graph node might be hidden/partially visible if the graph is overflowing the parent container.
|
||||
* Apart from the obvious problems in aesthetics, user do not get a proper feedback of the search result.
|
||||
* To solve this problem, we will have to scroll the node into view. (preferably into the center of the view)
|
||||
* Steps for that:
|
||||
* 1. Get the bounding rect of the node on graph.
|
||||
* 2. Get the midpoint of the node's bounding rect.
|
||||
* 3. Find the dimensions of the parent container.
|
||||
* 4. Since, we are trying to position the node into center, we set the left top corner position of parent to
|
||||
* below x and y.
|
||||
* x = node's x midpoint - half the width of parent container
|
||||
* y = node's y midpoint - half the height of parent container
|
||||
* 5. If the x and y are negative, we set them 0 as that is the minimum possible scroll position.
|
||||
* 6. Smoothly scroll to the left top x and y calculated in step 4, 5.
|
||||
*/
|
||||
|
||||
const cellRect = this.queryPlanView.azdataGraphDiagram.graph.getCellBounds(resultCell);
|
||||
const cellMidPoint: Point = {
|
||||
x: cellRect.x + cellRect.width / 2,
|
||||
y: cellRect.y + cellRect.height / 2,
|
||||
};
|
||||
|
||||
const graphContainer = <HTMLElement>this.queryPlanView.azdataGraphDiagram.container;
|
||||
const containerBoundingRect = graphContainer.getBoundingClientRect();
|
||||
|
||||
const leftTopScrollPoint: Point = {
|
||||
x: cellMidPoint.x - containerBoundingRect.width / 2,
|
||||
y: cellMidPoint.y - containerBoundingRect.height / 2
|
||||
};
|
||||
|
||||
leftTopScrollPoint.x = leftTopScrollPoint.x < 0 ? 0 : leftTopScrollPoint.x;
|
||||
leftTopScrollPoint.y = leftTopScrollPoint.y < 0 ? 0 : leftTopScrollPoint.y;
|
||||
|
||||
graphContainer.scrollTo({
|
||||
left: leftTopScrollPoint.x,
|
||||
top: leftTopScrollPoint.y,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
// Updating search result index based on prev flag
|
||||
if (returnPreviousResult) {
|
||||
// going to the end of list if the index is 0 on prev
|
||||
this._currentSearchResultIndex = this._currentSearchResultIndex === 0 ?
|
||||
this._currentSearchResultIndex = this._searchResults.length - 1 :
|
||||
this._currentSearchResultIndex = --this._currentSearchResultIndex;
|
||||
} else {
|
||||
// going to the front of list if we are at the last element
|
||||
this._currentSearchResultIndex = this._currentSearchResultIndex === this._searchResults.length - 1 ?
|
||||
this._currentSearchResultIndex = 0 :
|
||||
this._currentSearchResultIndex = ++this._currentSearchResultIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export class GoToNextMatchAction extends Action {
|
||||
public static ID = 'qp.NextSearchAction';
|
||||
public static LABEL = localize('nextSearchItemAction', "Next Match (Enter)");
|
||||
|
||||
constructor() {
|
||||
super(GoToNextMatchAction.ID, GoToNextMatchAction.LABEL, Codicon.arrowDown.classNames);
|
||||
}
|
||||
|
||||
public override async run(context: NodeSearchWidget): Promise<void> {
|
||||
context.searchNode(false);
|
||||
}
|
||||
}
|
||||
|
||||
export class GoToPreviousMatchAction extends Action {
|
||||
public static ID = 'qp.PreviousSearchAction';
|
||||
public static LABEL = localize('previousSearchItemAction', "Previous Match (Shift+Enter)");
|
||||
|
||||
constructor() {
|
||||
super(GoToPreviousMatchAction.ID, GoToPreviousMatchAction.LABEL, Codicon.arrowUp.classNames);
|
||||
}
|
||||
|
||||
public override async run(context: NodeSearchWidget): Promise<void> {
|
||||
context.searchNode(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class CancelSearch extends Action {
|
||||
public static ID = 'qp.cancelSearchAction';
|
||||
public static LABEL = localize('cancelSearchAction', "Close (Escape)");
|
||||
|
||||
constructor() {
|
||||
super(CancelSearch.ID, CancelSearch.LABEL, Codicon.chromeClose.classNames);
|
||||
}
|
||||
|
||||
public override async run(context: NodeSearchWidget): Promise<void> {
|
||||
context.queryPlanView.planActionView.removeWidget(context);
|
||||
}
|
||||
}
|
||||
@@ -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.11":
|
||||
version "0.0.11"
|
||||
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/efeb59abc720c33e35386749e4345af028613672"
|
||||
"azdataGraph@github:Microsoft/azdataGraph#0.0.14":
|
||||
version "0.0.14"
|
||||
resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/1fba9e94e5520ef78121f6dc23a5a2cdee20c8a4"
|
||||
|
||||
azure-storage@^2.10.2:
|
||||
version "2.10.2"
|
||||
|
||||
Reference in New Issue
Block a user