From 501c16f065471185bd9aa2e833bec4d5d661311e Mon Sep 17 00:00:00 2001 From: worksofliam Date: Sat, 15 Mar 2025 22:02:39 -0400 Subject: [PATCH 1/3] Initial work for graphs Signed-off-by: worksofliam --- src/views/cytoscape/index.ts | 149 +++++++++++++++++++++++++++++++++++ src/views/results/index.ts | 23 +++++- 2 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 src/views/cytoscape/index.ts diff --git a/src/views/cytoscape/index.ts b/src/views/cytoscape/index.ts new file mode 100644 index 00000000..f70c67fc --- /dev/null +++ b/src/views/cytoscape/index.ts @@ -0,0 +1,149 @@ +import { ViewColumn, window } from "vscode"; + +type Styles = {[key: string]: string}; + +export interface Element { + data: {id: string, label: string}, + style: Styles +} + +export interface Edge { + data: {id: string, source: string, target: string} +} + +interface NewNode { + label: string, + styles?: Styles, + parent?: string, + data?: any; +} + +const randomId = () => Math.random().toString(36).substring(7); + +export class CytoscapeGraph { + private elementData = new Map(); + private elements: Element[] = []; + private edges: Edge[] = []; + + constructor() {} + + addNode(node: NewNode): string { + const id = randomId(); // TODO: is this unique enough? + + if (node.data) { + this.elementData.set(id, node.data); + } + + this.elements.push({ + data: {id, label: node.label}, + style: node.styles || {} + }); + + if (node.parent) { + this.edges.push({ + data: {id: randomId(), source: node.parent, target: id} + }); + } + + return id; + } + + createView(title: string) { + const webview = window.createWebviewPanel(`c`, title, {viewColumn: ViewColumn.One}, {enableScripts: true, retainContextWhenHidden: true}); + webview.webview.html = this.getHtml(); + + return webview; + } + + private getHtml(): string { + return /*html*/` + + + + + + + + + + + +
+ + + + + + `; + } +} \ No newline at end of file diff --git a/src/views/results/index.ts b/src/views/results/index.ts index 113c2dcd..508c7a96 100644 --- a/src/views/results/index.ts +++ b/src/views/results/index.ts @@ -8,7 +8,7 @@ import { JobManager } from "../../config"; import Document from "../../language/sql/document"; import { ObjectRef, ParsedEmbeddedStatement, StatementGroup, StatementType } from "../../language/sql/types"; import Statement from "../../language/sql/statement"; -import { ExplainTree } from "./explain/nodes"; +import { ExplainNode, ExplainTree } from "./explain/nodes"; import { DoveResultsView, ExplainTreeItem } from "./explain/doveResultsView"; import { DoveNodeView, PropertyNode } from "./explain/doveNodeView"; import { DoveTreeDecorationProvider } from "./explain/doveTreeDecorationProvider"; @@ -17,6 +17,7 @@ import { generateSqlForAdvisedIndexes } from "./explain/advice"; import { updateStatusBar } from "../jobManager/statusBar"; import { ExplainType } from "@ibm/mapepire-js/dist/src/types"; import { DbCache } from "../../language/providers/logic/cache"; +import { CytoscapeGraph } from "../cytoscape"; export type StatementQualifier = "statement" | "update" | "explain" | "onlyexplain" | "json" | "csv" | "cl" | "sql"; @@ -256,6 +257,26 @@ async function runHandler(options?: StatementInfo) { const rootNode = doveResultsView.setRootNode(topLevel); doveNodeView.setNode(rootNode.explainNode); doveTreeDecorationProvider.updateTreeItems(rootNode); + + const graph = new CytoscapeGraph(); + + function addNode(node: ExplainNode, parent?: string) { + const id = graph.addNode({ + label: node.title, + parent: parent, + }); + + if (node.children) { + for (const child of node.children) { + addNode(child, id); + } + } + } + + addNode(topLevel); + + const webview = graph.createView(`Explain Graph`); + } else { vscode.window.showInformationMessage(`No job currently selected.`); } From 1bf06b016e276fdcec8d8a2028c950f1dc66cb0f Mon Sep 17 00:00:00 2001 From: worksofliam Date: Sat, 15 Mar 2025 22:10:04 -0400 Subject: [PATCH 2/3] Click on node to show detail Signed-off-by: worksofliam --- src/views/cytoscape/index.ts | 17 ++++++++++++++--- src/views/results/index.ts | 12 +++++++----- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/views/cytoscape/index.ts b/src/views/cytoscape/index.ts index f70c67fc..3f670ecb 100644 --- a/src/views/cytoscape/index.ts +++ b/src/views/cytoscape/index.ts @@ -48,10 +48,17 @@ export class CytoscapeGraph { return id; } - createView(title: string) { + createView(title: string, onNodeSelected: (data: unknown) => void): any { const webview = window.createWebviewPanel(`c`, title, {viewColumn: ViewColumn.One}, {enableScripts: true, retainContextWhenHidden: true}); webview.webview.html = this.getHtml(); + webview.webview.onDidReceiveMessage((message) => { + if (message.command === 'selected') { + const data = this.elementData.get(message.nodeId); + onNodeSelected(data); + } + }, undefined, []); + return webview; } @@ -90,6 +97,7 @@ export class CytoscapeGraph {
diff --git a/src/views/results/index.ts b/src/views/results/index.ts index 508c7a96..1da0b067 100644 --- a/src/views/results/index.ts +++ b/src/views/results/index.ts @@ -254,16 +254,14 @@ async function runHandler(options?: StatementInfo) { explainTree = new ExplainTree(explained.vedata); const topLevel = explainTree.get(); - const rootNode = doveResultsView.setRootNode(topLevel); - doveNodeView.setNode(rootNode.explainNode); - doveTreeDecorationProvider.updateTreeItems(rootNode); - + const graph = new CytoscapeGraph(); function addNode(node: ExplainNode, parent?: string) { const id = graph.addNode({ label: node.title, parent: parent, + data: node, }); if (node.children) { @@ -275,7 +273,11 @@ async function runHandler(options?: StatementInfo) { addNode(topLevel); - const webview = graph.createView(`Explain Graph`); + const webview = graph.createView(`Explain Graph`, (data: ExplainNode) => { + if (data) { + doveNodeView.setNode(data); + } + }); } else { vscode.window.showInformationMessage(`No job currently selected.`); From d81df5905664f06906a109d7a260ed776f3c54e7 Mon Sep 17 00:00:00 2001 From: worksofliam Date: Sat, 15 Mar 2025 22:29:29 -0400 Subject: [PATCH 3/3] Remove legacy view Signed-off-by: worksofliam --- package.json | 15 -- src/views/results/explain/contributes.json | 15 -- src/views/results/explain/doveResultsView.ts | 143 ------------------- src/views/results/explain/icons.ts | 48 +++++++ src/views/results/index.ts | 26 ---- 5 files changed, 48 insertions(+), 199 deletions(-) delete mode 100644 src/views/results/explain/doveResultsView.ts create mode 100644 src/views/results/explain/icons.ts diff --git a/package.json b/package.json index 3836c67a..75a6dab9 100644 --- a/package.json +++ b/package.json @@ -496,12 +496,6 @@ "category": "Db2 for i", "icon": "$(gear)" }, - { - "command": "vscode-db2i.dove.export", - "title": "Export current VE data", - "category": "Db2 for i", - "icon": "$(file)" - }, { "command": "vscode-db2i.dove.node.copy", "title": "Copy value", @@ -735,10 +729,6 @@ "command": "vscode-db2i.dove.editSettings", "when": "vscode-db2i:explaining == true" }, - { - "command": "vscode-db2i.dove.export", - "when": "vscode-db2i:explaining == true" - }, { "command": "vscode-db2i.dove.node.copy", "when": "never" @@ -835,11 +825,6 @@ "group": "navigation@1", "when": "view == vscode-db2i.dove.nodes" }, - { - "command": "vscode-db2i.dove.export", - "group": "navigation@2", - "when": "view == vscode-db2i.dove.nodes" - }, { "command": "vscode-db2i.dove.close", "group": "navigation@3", diff --git a/src/views/results/explain/contributes.json b/src/views/results/explain/contributes.json index 69383210..2d73944d 100644 --- a/src/views/results/explain/contributes.json +++ b/src/views/results/explain/contributes.json @@ -106,12 +106,6 @@ "category": "Db2 for i", "icon": "$(gear)" }, - { - "command": "vscode-db2i.dove.export", - "title": "Export current VE data", - "category": "Db2 for i", - "icon": "$(file)" - }, { "command": "vscode-db2i.dove.node.copy", "title": "Copy value", @@ -133,10 +127,6 @@ "command": "vscode-db2i.dove.editSettings", "when": "vscode-db2i:explaining == true" }, - { - "command": "vscode-db2i.dove.export", - "when": "vscode-db2i:explaining == true" - }, { "command": "vscode-db2i.dove.node.copy", "when": "never" @@ -153,11 +143,6 @@ "group": "navigation@1", "when": "view == vscode-db2i.dove.nodes" }, - { - "command": "vscode-db2i.dove.export", - "group": "navigation@2", - "when": "view == vscode-db2i.dove.nodes" - }, { "command": "vscode-db2i.dove.close", "group": "navigation@3", diff --git a/src/views/results/explain/doveResultsView.ts b/src/views/results/explain/doveResultsView.ts deleted file mode 100644 index fc9285a3..00000000 --- a/src/views/results/explain/doveResultsView.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as vscode from "vscode"; -import { CancellationToken, Event, EventEmitter, ProviderResult, TreeView, TreeDataProvider, TreeItem, TreeItemCollapsibleState, commands, ThemeIcon } from "vscode"; -import { ExplainNode } from "./nodes"; -import { toDoveTreeDecorationProviderUri } from "./doveTreeDecorationProvider"; - -/** - * Icon labels as defined by the API, along with the name of the icon to display. - * Not surprisingly, the reference link does not provide a complete list of icons. - * TODO: Add missing icons - * @see https://www.ibm.com/docs/en/i/7.5?topic=ssw_ibm_i_75/apis/qqqvexpl.html#icon_labels - * @see https://code.visualstudio.com/api/references/icons-in-labels - */ -const icons = { - "Bitmap Merge": `merge`, - "Cache": ``, - "Cache Probe": ``, - "Delete": `trash`, - "Distinct": `list-flat`, - "Dynamic Bitmap": `symbol-misc`, - "Encoded Vector Index": `symbol-reference`, - "Encoded Vector Index, Parallel": `symbol-reference`, - "Final Select": `selection`, - "Hash Grouping": `group-by-ref-type`, - "Hash Join": `add`, - "Hash Scan": `search`, - "Index Grouping": `group-by-ref-type`, - "Index Scan - Key Positioning": `key`, - "Index Scan - Key Positioning, Parallel": `key`, - "Index Scan - Key Selection": `key`, - "Index Scan - Key Selection, Parallel": `key`, - "Insert": `insert`, - "Nested Loop Join": `add`, - "Select": `selection`, - "Skip Sequential Table Scan": `list-unordered`, - "Skip Sequential Table Scan, Parallel": `list-unordered`, - "Sort": `sort-precedence`, - "Sorted List Scan": `list-ordered`, - "Subquery Merge": `merge`, - "Table Probe": `list-selection`, - "Table Scan": `search`, - "Table Scan, Parallel": `search`, - "Temporary Distinct Hash Table": `new-file`, - "Temporary Hash Table": `new-file`, - "Temporary Index": `new-file`, - "Temporary Sorted List": `list-ordered`, - "Temporary Table": `new-file`, - "Union Merge": `merge`, - "User Defined Table Function": `symbol-function`, - "Unknown": `question`, - "Update": `replace`, - "VALUES LIST": `list-flat`, -} - -type ChangeTreeDataEventType = ExplainTreeItem | undefined | null | void; - -export class DoveResultsView implements TreeDataProvider { - private _onDidChangeTreeData: EventEmitter = new EventEmitter(); - readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event; - - private topNode: ExplainTreeItem; - - private treeView: TreeView; - - constructor() { - this.treeView = vscode.window.createTreeView(`vscode-db2i.dove.nodes`, { treeDataProvider: this, showCollapseAll: true }); - } - - public getTreeView(): TreeView { - return this.treeView; - } - - setRootNode(topNode: ExplainNode): ExplainTreeItem { - this.topNode = new ExplainTreeItem(topNode); - this._onDidChangeTreeData.fire(); - - // Show tree in the view - commands.executeCommand(`setContext`, `vscode-db2i:explaining`, true); - // Ensure that the tree is positioned such that the first element is visible - this.treeView.reveal(this.topNode, { select: false }); - return this.topNode; - } - getRootNode(): ExplainTreeItem { - return this.topNode; - } - - getRootExplainNode(): ExplainNode { - return this.topNode.explainNode; - } - - close(): void { - commands.executeCommand(`setContext`, `vscode-db2i:explaining`, false); - } - - getTreeItem(element: ExplainTreeItem): ExplainTreeItem | Thenable { - return element; - } - - getChildren(element?: ExplainTreeItem): ProviderResult { - if (element) { - return element.getChildren(); - } else if (this.topNode) { - return [this.topNode]; - } else { - return []; - } - } - - getParent?(element: any) { - throw new Error("Method not implemented."); - } - - resolveTreeItem?(item: TreeItem, element: any, token: CancellationToken): ProviderResult { - throw new Error("Method not implemented."); - } -} - -export class ExplainTreeItem extends TreeItem { - explainNode: ExplainNode; - private children: ExplainTreeItem[]; - - constructor(node: ExplainNode) { - super(node.title, node.childrenNodes > 0 ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None); - this.explainNode = node; - this.contextValue = `explainTreeItem`; - - // If the node is associated with a DB object, display the qualified object name in the description - if (node.objectSchema && node.objectName) { - this.description = node.objectSchema + `.` + node.objectName; - } - - // TODO: ideally the tooltip would be built using a MarkdownString, but regardless of everything tried, 'Loading...' is always displayed - this.tooltip = [node.title, node.tooltipProps.map(prop => prop.title + `: ` + prop.value).join(`\n`), ``].join(`\n`); - this.resourceUri = toDoveTreeDecorationProviderUri(node.highlights); - this.iconPath = new ThemeIcon(icons[node.title] || `server-process`, node.highlights.getPriorityColor()); // `circle-outline` - } - - getChildren(): ExplainTreeItem[] { - if (!this.children) { - this.children = this.explainNode.children.map(c => new ExplainTreeItem(c)); - } - return this.children; - } -} \ No newline at end of file diff --git a/src/views/results/explain/icons.ts b/src/views/results/explain/icons.ts new file mode 100644 index 00000000..65cb4115 --- /dev/null +++ b/src/views/results/explain/icons.ts @@ -0,0 +1,48 @@ + +/** + * Icon labels as defined by the API, along with the name of the icon to display. + * Not surprisingly, the reference link does not provide a complete list of icons. + * TODO: Add missing icons + * @see https://www.ibm.com/docs/en/i/7.5?topic=ssw_ibm_i_75/apis/qqqvexpl.html#icon_labels + * @see https://code.visualstudio.com/api/references/icons-in-labels + */ +const icons = { + "Bitmap Merge": `merge`, + "Cache": ``, + "Cache Probe": ``, + "Delete": `trash`, + "Distinct": `list-flat`, + "Dynamic Bitmap": `symbol-misc`, + "Encoded Vector Index": `symbol-reference`, + "Encoded Vector Index, Parallel": `symbol-reference`, + "Final Select": `selection`, + "Hash Grouping": `group-by-ref-type`, + "Hash Join": `add`, + "Hash Scan": `search`, + "Index Grouping": `group-by-ref-type`, + "Index Scan - Key Positioning": `key`, + "Index Scan - Key Positioning, Parallel": `key`, + "Index Scan - Key Selection": `key`, + "Index Scan - Key Selection, Parallel": `key`, + "Insert": `insert`, + "Nested Loop Join": `add`, + "Select": `selection`, + "Skip Sequential Table Scan": `list-unordered`, + "Skip Sequential Table Scan, Parallel": `list-unordered`, + "Sort": `sort-precedence`, + "Sorted List Scan": `list-ordered`, + "Subquery Merge": `merge`, + "Table Probe": `list-selection`, + "Table Scan": `search`, + "Table Scan, Parallel": `search`, + "Temporary Distinct Hash Table": `new-file`, + "Temporary Hash Table": `new-file`, + "Temporary Index": `new-file`, + "Temporary Sorted List": `list-ordered`, + "Temporary Table": `new-file`, + "Union Merge": `merge`, + "User Defined Table Function": `symbol-function`, + "Unknown": `question`, + "Update": `replace`, + "VALUES LIST": `list-flat`, +} \ No newline at end of file diff --git a/src/views/results/index.ts b/src/views/results/index.ts index 1da0b067..1ae6da77 100644 --- a/src/views/results/index.ts +++ b/src/views/results/index.ts @@ -9,7 +9,6 @@ import Document from "../../language/sql/document"; import { ObjectRef, ParsedEmbeddedStatement, StatementGroup, StatementType } from "../../language/sql/types"; import Statement from "../../language/sql/statement"; import { ExplainNode, ExplainTree } from "./explain/nodes"; -import { DoveResultsView, ExplainTreeItem } from "./explain/doveResultsView"; import { DoveNodeView, PropertyNode } from "./explain/doveNodeView"; import { DoveTreeDecorationProvider } from "./explain/doveTreeDecorationProvider"; import { ResultSetPanelProvider } from "./resultSetPanelProvider"; @@ -42,8 +41,6 @@ export function setCancelButtonVisibility(visible: boolean) { let resultSetProvider = new ResultSetPanelProvider(); let explainTree: ExplainTree; -let doveResultsView = new DoveResultsView(); -let doveResultsTreeView: TreeView = doveResultsView.getTreeView(); let doveNodeView = new DoveNodeView(); let doveNodeTreeView: TreeView = doveNodeView.getTreeView(); let doveTreeDecorationProvider = new DoveTreeDecorationProvider(); // Self-registers as a tree decoration providor @@ -52,7 +49,6 @@ export function initialise(context: vscode.ExtensionContext) { setCancelButtonVisibility(false); context.subscriptions.push( - doveResultsTreeView, doveNodeTreeView, vscode.window.registerWebviewViewProvider(`vscode-db2i.resultset`, resultSetProvider, { @@ -91,17 +87,6 @@ export function initialise(context: vscode.ExtensionContext) { } }), - vscode.commands.registerCommand(`vscode-db2i.dove.close`, () => { - doveResultsView.close(); - doveNodeView.close(); - }), - - vscode.commands.registerCommand(`vscode-db2i.dove.displayDetails`, (explainTreeItem: ExplainTreeItem) => { - // When the user clicks for details of a node in the tree, set the focus to that node as a visual indicator tying it to the details tree - doveResultsTreeView.reveal(explainTreeItem, { select: false, focus: true, expand: true }); - doveNodeView.setNode(explainTreeItem.explainNode); - }), - vscode.commands.registerCommand(`vscode-db2i.dove.node.copy`, (propertyNode: PropertyNode) => { if (propertyNode.description && typeof propertyNode.description === `string`) { vscode.env.clipboard.writeText(propertyNode.description); @@ -117,15 +102,6 @@ export function initialise(context: vscode.ExtensionContext) { vscode.commands.executeCommand('workbench.action.openSettings', 'vscode-db2i.visualExplain'); }), - vscode.commands.registerCommand(`vscode-db2i.dove.export`, () => { - vscode.workspace.openTextDocument({ - language: `json`, - content: JSON.stringify(doveResultsView.getRootExplainNode(), null, 2) - }).then(doc => { - vscode.window.showTextDocument(doc); - }); - }), - vscode.commands.registerCommand(`vscode-db2i.dove.generateSqlForAdvisedIndexes`, () => { const scriptContent = generateSqlForAdvisedIndexes(explainTree); @@ -158,8 +134,6 @@ async function runHandler(options?: StatementInfo) { const optionsIsValid = (options?.content !== undefined); let editor = vscode.window.activeTextEditor; - vscode.commands.executeCommand('vscode-db2i.dove.close'); - if (optionsIsValid || (editor && editor.document.languageId === `sql`)) { let chosenView = resultSetProvider;