diff --git a/package.json b/package.json index eb3a6099..85186680 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,11 @@ "title": "General", "order": 1, "properties": { + "vscode-db21.selfCodePanelTimeout": { + "type": "number", + "description": "Timeout in seconds for the SELF code panel to be shown", + "default": 3 + }, "vscode-db2i.pageSize": { "type": "number", "description": "Page size for Schema browser", @@ -172,7 +177,15 @@ "id": "vscode-db2i.resultset", "name": "Results", "when": "code-for-ibmi:connected == true", - "contextualTitle": "DB2 for i" + "contextualTitle": "IBM i" + }, + { + "type": "webview", + "id": "vscode-db2i.selfCodeErrorPanel", + "name": "Self Code Errors", + "when": "code-for-ibmi:connected == true && vscode-db2i:selfCodeCountChanged == true", + "contextualTitle": "DB2 for i", + "visibility": "collapsed" }, { "type": "tree", diff --git a/src/connection/manager.ts b/src/connection/manager.ts index baddaa5b..cbd00982 100644 --- a/src/connection/manager.ts +++ b/src/connection/manager.ts @@ -90,6 +90,14 @@ export class SQLJobManager { const jobExists = this.jobs.findIndex(info => info.name === selectedName); this.selectedJob = jobExists; + if (jobExists >= 0) { + const curJob: JobInfo = this.jobs[jobExists]; + if (curJob.job.options.selfcodes) { + curJob.job.setSelfCodes(curJob.job.options.selfcodes); + } else { + curJob.job.setSelfCodes([]); + } + } return (this.selectedJob >= 0); } diff --git a/src/connection/sqlJob.ts b/src/connection/sqlJob.ts index 27129054..90780ada 100644 --- a/src/connection/sqlJob.ts +++ b/src/connection/sqlJob.ts @@ -271,7 +271,6 @@ export class SQLJob { throw e; } } - async setTraceConfig(dest: ServerTraceDest, level: ServerTraceLevel): Promise { const reqObj = { id: SQLJob.getNewUniqueId(), diff --git a/src/views/jobManager/ConfigManager.ts b/src/views/jobManager/ConfigManager.ts index d71c9324..0adf76fd 100644 --- a/src/views/jobManager/ConfigManager.ts +++ b/src/views/jobManager/ConfigManager.ts @@ -42,7 +42,21 @@ export class ConfigManager { const options = this.getConfig(name); if (options) { - commands.executeCommand(`vscode-db2i.jobManager.newJob`, options, name); + await window.withProgress({ location: ProgressLocation.Window }, async (progress) => { + try { + progress.report({ message: `Spinning up SQL job...` }); + const newJob: SQLJob = new SQLJob(options); + await JobManager.newJob(newJob, name); + if (options.selfcodes) { + newJob.setSelfCodes(options.selfcodes); + commands.executeCommand(`setContext`, `vscode-db2i:selfCodeCountChanged`, false); + } + } catch (e) { + window.showErrorMessage(e.message); + } + + this.refresh(); + }); } }), diff --git a/src/views/jobManager/jobManagerView.ts b/src/views/jobManager/jobManagerView.ts index 25ccbfaf..604f1fc9 100644 --- a/src/views/jobManager/jobManagerView.ts +++ b/src/views/jobManager/jobManagerView.ts @@ -32,7 +32,8 @@ export class JobManagerView implements TreeDataProvider { await JobManager.newJob( (options ? new SQLJob(options) : undefined), name - ); + ); + commands.executeCommand(`setContext`, `vscode-db2i:selfCodeCountChanged`, false); } catch (e) { window.showErrorMessage(e.message); } @@ -189,13 +190,7 @@ export class JobManagerView implements TreeDataProvider { const id = node.label as string; const selected = await JobManager.getJob(id); - const content = `SELECT * FROM QSYS2.SQL_ERROR_LOG WHERE JOB_NAME = '${selected.job.id}'`; - - vscode.commands.executeCommand(`vscode-db2i.runEditorStatement`, { - content, - qualifier: `statement`, - open: false, - }); + vscode.commands.executeCommand(`vscode-db2i.selfCodeErrorPanel.fetch`, selected.job, true); } }), diff --git a/src/views/results/html.ts b/src/views/results/html.ts index 07b3a83e..a4c633c1 100644 --- a/src/views/results/html.ts +++ b/src/views/results/html.ts @@ -38,6 +38,34 @@ export function getLoadingHTML(): string { `; } +export function getSelfCodeHelp(): string { + return /*html*/ ` + + + + ${getHeader()} + + + +
+

Under active SQL Job, run Get SELF codes Errors to update view

+ +
+ + + `; +} + export function generateScroller(basicSelect: string, isCL: boolean): string { const withCollapsed = Configuration.get('collapsedResultSet'); @@ -141,29 +169,48 @@ export function generateScroller(basicSelect: string, isCL: boolean): string { }); } + function isJsonString(str) { + try { + JSON.parse(str); + return true; + } catch (e) { + return false; + } + } + + function getFormattedCell(cellValue) { + // Handle undefined cells + var formattedValue = cellValue === undefined ? 'null' : cellValue; + + // Format JSON cells + if (isJsonString(formattedValue)) { + var formattedJson = JSON.stringify(JSON.parse(formattedValue), null, 2); + var pre = document.createElement('pre'); + pre.style.maxHeight = '100px'; + pre.style.overflowY = 'auto'; + pre.style.margin = '0'; + pre.textContent = formattedJson; + return pre; + } else { + // Handle non-JSON cells + return document.createTextNode(formattedValue); + } + } + function appendRows(tableId, arrayOfObjects) { var tBodyRef = document.getElementById(tableId).getElementsByTagName('tbody')[0]; for (const row of arrayOfObjects) { - // Insert a row at the end of table - var newRow = tBodyRef.insertRow() + var newRow = tBodyRef.insertRow(); for (const cell of row) { - // Insert a cell at the end of the row var newCell = newRow.insertCell(); - - // Append a text node to the cell - - //TODO: handle cell formatting here - var newDiv = document.createElement("div"); - newDiv.className = "hoverable"; - newDiv.appendChild(document.createTextNode(cell === undefined ? 'null' : cell)); - - newCell.appendChild(newDiv); + var cellContent = getFormattedCell(cell); + newCell.appendChild(cellContent); } } - } + @@ -179,4 +226,117 @@ export function generateScroller(basicSelect: string, isCL: boolean): string { `; +} + +export function generateDynamicTable(): string { + // const parsedData = JSON.parse(data); + + return /*html*/` + + + + ${getHeader({withCollapsed: false})} + + + + + + +
+

+ + + `; } \ No newline at end of file diff --git a/src/views/results/index.ts b/src/views/results/index.ts index eed9dc23..2c852dd1 100644 --- a/src/views/results/index.ts +++ b/src/views/results/index.ts @@ -1,12 +1,8 @@ -import vscode, { SnippetString, ViewColumn, TreeView } from "vscode" +import vscode, { EndOfLine, SnippetString, ViewColumn, TreeView } from "vscode"; import * as csv from "csv/sync"; - -import { JobManager } from "../../config"; -import Document from "../../language/sql/document"; import { changedCache } from "../../language/providers/completionItemCache"; -import { ParsedEmbeddedStatement, StatementGroup, StatementType } from "../../language/sql/types"; import Statement from "../../language/sql/statement"; import { ExplainTree, ContextType } from "./explain/nodes"; import { DoveResultsView, ExplainTreeItem } from "./explain/doveResultsView"; @@ -16,6 +12,13 @@ import { ResultSetPanelProvider } from "./resultSetPanelProvider"; import { ExplainType } from "../../connection/sqlJob"; export type StatementQualifier = "statement" | "explain" | "onlyexplain" | "json" | "csv" | "cl" | "sql"; +import { ParsedEmbeddedStatement, StatementGroup, StatementType } from "../../language/sql/types"; +import { JobManager } from "../../config"; +import Document from "../../language/sql/document"; + +export function delay(t: number, v?: number) { + return new Promise(resolve => setTimeout(resolve, t, v)); +} export interface StatementInfo { content: string, @@ -41,6 +44,8 @@ let doveNodeTreeView: TreeView = doveNodeView.getTreeView(); let doveTreeDecorationProvider = new DoveTreeDecorationProvider(); // Self-registers as a tree decoration providor export function initialise(context: vscode.ExtensionContext) { + let resultSetProvider = new ResultSetPanelProvider(); + context.subscriptions.push( vscode.window.registerWebviewViewProvider(`vscode-db2i.resultset`, resultSetProvider, { webviewOptions: { retainContextWhenHidden: true }, @@ -56,7 +61,7 @@ export function initialise(context: vscode.ExtensionContext) { 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 }); + doveResultsTreeView.reveal(explainTreeItem, { select: false, focus: true, expand: true }); doveNodeView.setNode(explainTreeItem.explainNode); }), @@ -187,61 +192,62 @@ async function runHandler(options?: StatementInfo) { if (data.length > 0) { switch (statementDetail.qualifier) { - case `csv`: - case `json`: - case `sql`: - let content = ``; - switch (statementDetail.qualifier) { - case `csv`: content = csv.stringify(data, { - header: true, - quoted_string: true, - }); break; - case `json`: content = JSON.stringify(data, null, 2); break; - + case `csv`: + case `json`: case `sql`: - const keys = Object.keys(data[0]); - - // split array into groups of 1k - const insertLimit = 1000; - const dataChunks = []; - for (let i = 0; i < data.length; i += insertLimit) { - dataChunks.push(data.slice(i, i + insertLimit)); + let content = ``; + switch (statementDetail.qualifier) { + case `csv`: content = csv.stringify(data, { + header: true, + quoted_string: true, + }); break; + case `json`: content = JSON.stringify(data, null, 2); break; + + case `sql`: + const keys = Object.keys(data[0]); + + // split array into groups of 1k + const insertLimit = 1000; + const dataChunks = []; + for (let i = 0; i < data.length; i += insertLimit) { + dataChunks.push(data.slice(i, i + insertLimit)); + } + + content = `-- Generated ${dataChunks.length} insert statement${dataChunks.length === 1 ? `` : `s`}\n\n`; + + for (const data of dataChunks) { + const insertStatement = [ + `insert into TABLE (`, + ` ${keys.join(`, `)}`, + `) values `, + data.map( + row => ` (${keys.map(key => { + if (row[key] === null) return `null`; + if (typeof row[key] === `string`) return `'${String(row[key]).replace(/'/g, `''`)}'`; + return row[key]; + }).join(`, `)})` + ).join(`,\n`), + ]; + content += insertStatement.join(`\n`) + `;\n`; + } + break; } - content = `-- Generated ${dataChunks.length} insert statement${dataChunks.length === 1 ? `` : `s`}\n\n`; - - for (const data of dataChunks) { - const insertStatement = [ - `insert into TABLE (`, - ` ${keys.join(`, `)}`, - `) values `, - data.map( - row => ` (${keys.map(key => { - if (row[key] === null) return `null`; - if (typeof row[key] === `string`) return `'${String(row[key]).replace(/'/g, `''`)}'`; - return row[key]; - }).join(`, `)})` - ).join(`,\n`), - ]; - content += insertStatement.join(`\n`) + `;\n`; - } + const textDoc = await vscode.workspace.openTextDocument({ language: statementDetail.qualifier, content }); + await vscode.window.showTextDocument(textDoc); + resultSetProvider.setLoadingText(`Query executed with ${data.length} rows returned.`); break; - } - - const textDoc = await vscode.workspace.openTextDocument({ language: statementDetail.qualifier, content }); - await vscode.window.showTextDocument(textDoc); - resultSetProvider.setLoadingText(`Query executed with ${data.length} rows returned.`); - break; } } else { vscode.window.showInformationMessage(`Query executed with no data returned.`); - resultSetProvider.setLoadingText(`Query executed with no data returned.`); } } - if ((statementDetail.qualifier === `statement` || statementDetail.qualifier === `explain`) && statementDetail.history !== false) { + + if (statementDetail.qualifier === `statement` && statementDetail.history !== false) { vscode.commands.executeCommand(`vscode-db2i.queryHistory.prepend`, statementDetail.content); } + } catch (e) { let errorText; if (typeof e === `string`) { @@ -257,6 +263,7 @@ async function runHandler(options?: StatementInfo) { } } } + } } diff --git a/src/views/results/resultSetPanelProvider.ts b/src/views/results/resultSetPanelProvider.ts index 777315a0..8c0bff2a 100644 --- a/src/views/results/resultSetPanelProvider.ts +++ b/src/views/results/resultSetPanelProvider.ts @@ -83,7 +83,7 @@ export class ResultSetPanelProvider { } } - async setLoadingText(content) { + async setLoadingText(content: string) { await this.focus(); if (!this.loadingState) { @@ -95,7 +95,6 @@ export class ResultSetPanelProvider { } async setScrolling(basicSelect, isCL = false, queryId: string = ``) { - this.loadingState = false; await this.focus(); this._view.webview.html = html.generateScroller(basicSelect, isCL); diff --git a/src/views/results/selfCodePanel.ts b/src/views/results/selfCodePanel.ts new file mode 100644 index 00000000..3801d869 --- /dev/null +++ b/src/views/results/selfCodePanel.ts @@ -0,0 +1,66 @@ +import * as vscode from 'vscode'; +import * as html from "./html"; +import { delay } from "./index"; + +export class SelfCodePanelProvider { + _view: vscode.WebviewView; + loadingState: boolean; + selfCodeCache: number = 0; + constructor() { + this._view = undefined; + this.loadingState = false; + } + + resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, _token: vscode.CancellationToken) { + this._view = webviewView; + + webviewView.webview.options = { + // Allow scripts in the webview + enableScripts: true, + }; + webviewView.webview.html = html.getSelfCodeHelp(); + } + + async ensureActivation() { + let currentLoop = 0; + while (!this._view && currentLoop < 15) { + await this.focus(); + await delay(100); + currentLoop += 1; + } + } + + async focus() { + if (!this._view) { + // Weird one. Kind of a hack. _view.show doesn't work yet because it's not initialized. + // But, we can call a VS Code API to focus on the tab, which then + // 1. calls resolveWebviewView + // 2. sets this._view + await vscode.commands.executeCommand(`vscode-db2i.resultset.focus`); + } else { + this._view.show(true); + } + } + + async setTableData(data: any[]) { + await this.focus(); + + const rows = Object.values(data).map(obj => Object.values(obj)); + const cols = Object.keys(data[0]); + + const rawhtml = html.generateDynamicTable(); + + this._view.webview.html = rawhtml; + this._view.webview.postMessage({ + command: 'setTableData', + rows: rows, + columnList: cols + }); + } + + setError(error) { + // TODO: pretty error + this._view.webview.html = `

${error}

`; + } +} +