diff --git a/CHANGELOG.md b/CHANGELOG.md index fe9e995c..309b58b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,44 @@ All notable changes to the **kdb VS Code extension** are documented in this file. +# v1.6.0 + +### Enhancements + +- Display meta data for Insights connections +- Added option to click at meta data and open in json format the meta data +- Ability to change the name of the Keycloak realm, used for authentication, from the default value of `insights`. This enables the connection to a kdb Insights Enterprise Free trial instance. +- Improve the console log quality to "kdb"output pane +- Insights free trial instances are supported +- Added execute block command for q code +- Added hotkey to cache function parameters for q code +- Extension now reconigze which version of Insights is connected +- Extension changes scratchpad endpoints accordly to the Insights versions +- Allow connection information in user settings to be editable +- Allow same server address to be used in multiple connections +- Language server features works on unsaved files +- Expand Selection command is implemented + +### Fixes + +- Disconnect when q process is stopped +- Fix query execution on KDB+ connections not refreshing completion items +- Fixed delay when executing query on KDB+ connections +- Make connection names case insensitive +- Fixed GUID type displayed as number for Insights +- Fixed problem when the user close(not hide) the Results Tab +- Fixed time zone for populate scratchpad + +### Internal Improvements + +- Added logging framework + # v1.5.2 +### Fixes + - Local connection listener behaviour fixed (if the connection is closed, the connection will disconnect) -- Return to show console output if results tab isn't visible in case of query execuion +- Return to show console output if results tab isn't visible in case of query execution - Linter fixes # v1.5.1 diff --git a/README.md b/README.md index 5f8e961a..4341a9d7 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,17 @@ Set the following properties: ![connecttoinsights](https://github.com/KxSystems/kx-vscode/blob/main/img/insightsconnection.png?raw=true) +Set the following from the Advanced properties if necessary: + +| Property | Description | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Define Realm | Specify the Keycloak realm for authentication. Usually the realm is set to `insights`, which is the default value used by the extension. You only need to change this field if a different realm has been configured on your server. | + +![connecttoinsights](https://github.com/KxSystems/kx-vscode/blob/main/img/insightsconnectionadvanced.png?raw=true) + +!!!note "For kdb Insights Enterprise Free Trial instances" +The realm is configured as `insights-{URL}` where {URL} is the 10 digit code in the trial URL. For example: if your trial url is https://fstc83yi5b.ft1.cld.kx.com/ the realm should be `insights-fstc83yi5b`. + 1. Click **Create Connection** and the **kdb Insights Enterprise** connection appears under **CONNECTIONS** in the primary sidebar. 1. Right-click the connection, and click **Connect server**. @@ -264,6 +275,8 @@ For any file with a **.q** or **.py** extension there are additional options ava - **Execute current selection** - Takes the current selection (or current line if nothing is selected) and executes it against the active connection. Results are displayed in the [Output window and/or the KDB Results window](#view-results). +- **Execute current block** - Selects the q expression under the cursor and executes it against the active connection. Results are displayed in the [Output window and/or the KDB Results window](#view-results). + - **Run q file in new q instance** - If q is installed and executable from the terminal you can execute an entire q script on a newly launched q instance. Executing a file on a new instance is done in the terminal, and allows interrogation of the active q process from the terminal window. ### Insights query execution @@ -330,6 +343,16 @@ To do this: 1. Use a [Workbook](#workbooks) to execute q or Python code against the data in your scratchpad using the variable you provided. +## Meta + +The Get Meta data is exposed for **connected Insights** connections. + +![Insights Meta Tree](https://github.com/KxSystems/kx-vscode/blob/main/img/insights-meta-tree.png?raw=true) + +To open the meta object, just click on it, and a json with the **"[Connection Name] - [META OBJECT]"** as title + +![Insights Meta JSON](https://github.com/KxSystems/kx-vscode/blob/main/img/insights-meta-json.png?raw=true) + ## Workbooks Workbooks provide a convenient way to prototype and execute q and python code against a q process and using the variables [populated into the scratchpad](#populate-scratchpad) of a **kdb Insights Enterprise** deployment by data sources. @@ -359,7 +382,7 @@ To create a Workbook and run code against a specific connection: 1. Click **Run** from above the first line of code in the workbook file. ![workbook links](https://github.com/KxSystems/kx-vscode/blob/main/img/workbookrunlink.png?raw=true) - 1. Select **Run** from the upper right of the editor. Using the dropdown next to the button you can choose to **KX: Execute Entire File** or **KX Execute Current Selection**. + 1. Select **Run** from the upper right of the editor. Using the dropdown next to the button you can choose any of the [**KX:** menu items](#kdb-process-executing-q-and-python-code) to run some, or all of the code in the workbook. ![play dropdown](https://github.com/KxSystems/kx-vscode/blob/main/img/wortkbookplaydropdown.png?raw=true) 1. Click **Run** on the right-hand side of the status bar. @@ -367,8 +390,6 @@ To create a Workbook and run code against a specific connection: 1. Right-click and choose **KX: Execute Entire File** from the menu. - 1. If you wish to only run the current selection (or current line if nothing is selected), right-click and choose **KX: Execute Current Selection** from the menu. - 1. If you have not yet chosen a connection to associate with the workbook you are asked to choose a connection before the code is executed. ![choose connection](https://github.com/KxSystems/kx-vscode/blob/main/img/workbookconnectionlink.png?raw=true) @@ -429,6 +450,16 @@ q REPL can be started from the command prompt by searching **q REPL**. ![REPL](https://github.com/KxSystems/kx-vscode/blob/main/img/repl.png?raw=true) +## Logs + +Any error or info will be posted at **OUTPUT** in **kdb** tab + +![LOG](https://github.com/KxSystems/kx-vscode/blob/main/img/log-sample.png?raw=true) + +The format will be: + +`[DATE TIME] [INFO or ERROR] Message` + ## Settings To update kdb VS Code settings, search for **kdb** from _Preferences_ > _Settings_, or right-click the settings icon in kdb VS Code marketplace panel and choose **Extension Settings**. @@ -502,26 +533,40 @@ To update kdb VS Code settings, search for **kdb** from _Preferences_ > _Setting } ``` +### Double Click Selection + +The following setting will change double click behaviour to select the whole identifier including dots: + +```JSON + "[q]": { + "editor.wordSeparators": "`~!@#$%^&*()-=+[{]}\\|;:'\",<>/?" + } +``` + ## Shortcuts ### For Windows -| Key | Action | -| ------------------ | ---------------------------- | -| F12 | Go to definition | -| Shift + F12 | Go to references | -| Ctrl + Shift + F12 | Find all references | -| Ctrl + D | Execute current selection | -| Ctrl + Shift + D | Execute entire file | -| Ctrl + Shift + R | Run q file in new q instance | +| Key | Action | +| ------------------ | --------------------------------- | +| F12 | Go to definition | +| Shift + F12 | Go to references | +| Ctrl + Shift + F12 | Find all references | +| Ctrl + D | Execute current selection | +| Ctrl + Shift + E | Execute current block | +| Ctrl + Shift + D | Execute entire file | +| Ctrl + Shift + R | Run q file in new q instance | +| Ctrl + Shift + Y | Toggle paramater cache for lambda | ### For MacOS -| Key | Action | -| --------------- | ---------------------------- | -| F12 | Go to definition | -| Shift + F12 | Go to references | -| ⌘ + Shift + F12 | Find all references | -| ⌘ + D | Execute current selection | -| ⌘ + Shift + D | Execute entire file | -| ⌘ + Shift + R | Run q file in new q instance | +| Key | Action | +| --------------- | --------------------------------- | +| F12 | Go to definition | +| Shift + F12 | Go to references | +| ⌘ + Shift + F12 | Find all references | +| ⌘ + D | Execute current selection | +| ⌘ + Shift + E | Execute current block | +| ⌘ + Shift + D | Execute entire file | +| ⌘ + Shift + R | Run q file in new q instance | +| ⌘ + Shift + Y | Toggle paramater cache for lambda | diff --git a/img/insights-meta-json.png b/img/insights-meta-json.png new file mode 100644 index 00000000..71d4035f Binary files /dev/null and b/img/insights-meta-json.png differ diff --git a/img/insights-meta-tree.png b/img/insights-meta-tree.png new file mode 100644 index 00000000..2d0e4d27 Binary files /dev/null and b/img/insights-meta-tree.png differ diff --git a/img/insightsconnection.png b/img/insightsconnection.png index d207bef8..233c4f30 100644 Binary files a/img/insightsconnection.png and b/img/insightsconnection.png differ diff --git a/img/insightsconnectionadvanced.png b/img/insightsconnectionadvanced.png new file mode 100644 index 00000000..1a77b5a7 Binary files /dev/null and b/img/insightsconnectionadvanced.png differ diff --git a/img/log-sample.png b/img/log-sample.png new file mode 100644 index 00000000..bea43a7d Binary files /dev/null and b/img/log-sample.png differ diff --git a/img/workbookplaydropdown.png b/img/workbookplaydropdown.png index cad847b0..5dfbdf46 100644 Binary files a/img/workbookplaydropdown.png and b/img/workbookplaydropdown.png differ diff --git a/package-lock.json b/package-lock.json index 35abd593..e324d91c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kdb", - "version": "1.5.2", + "version": "1.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kdb", - "version": "1.5.2", + "version": "1.6.0", "license": "MIT", "dependencies": { "@types/graceful-fs": "^4.1.9", diff --git a/package.json b/package.json index 3577ac71..87b0a21b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "kdb", "description": "IDE support for kdb product suite", "publisher": "KX", - "version": "1.5.2", + "version": "1.6.0", "engines": { "vscode": "^1.86.0" }, @@ -183,7 +183,7 @@ { "category": "KX", "command": "kdb.refreshServerObjects", - "title": "Refresh server objects", + "title": "Refresh server objects & insights meta", "icon": "$(refresh)" }, { @@ -263,6 +263,11 @@ "command": "kdb.connect", "title": "Connect server" }, + { + "category": "KX", + "command": "kdb.insights.refreshMeta", + "title": "Refresh get meta" + }, { "category": "KX", "command": "kdb.connect.via.dialog", @@ -273,6 +278,11 @@ "command": "kdb.active.connection", "title": "Active connection" }, + { + "category": "KX", + "command": "kdb.open.meta", + "title": "Open meta object" + }, { "category": "KX", "command": "kdb.addAuthentication", @@ -374,6 +384,17 @@ "category": "KX", "command": "kdb.deleteFile", "title": "Delete" + }, + { + "category": "KX", + "command": "kdb.execute.block", + "title": "KX: Execute Current q Block", + "icon": "$(run-above)" + }, + { + "category": "KX", + "command": "kdb.toggleParameterCache", + "title": "KX: Toggle parameter cache" } ], "keybindings": [ @@ -405,6 +426,18 @@ "key": "ctrl+shift+r", "mac": "cmd+shift+r", "when": "editorLangId == q && !(resourceFilename =~ /.kdb.q/)" + }, + { + "command": "kdb.execute.block", + "key": "ctrl+shift+e", + "mac": "cmd+shift+e", + "when": "editorLangId == q && !(resourceFilename =~ /.kdb.q/)" + }, + { + "command": "kdb.toggleParameterCache", + "key": "ctrl+shift+y", + "mac": "cmd+shift+y", + "when": "editorLangId == q && !(resourceFilename =~ /.kdb.q/)" } ], "snippets": [ @@ -512,6 +545,10 @@ "command": "kdb.insightsRemove", "when": "false" }, + { + "command": "kdb.insights.refreshMeta", + "when": "false" + }, { "command": "kdb.startLocalProcess", "when": "false" @@ -532,6 +569,10 @@ "command": "kdb.connect", "when": "false" }, + { + "command": "kdb.open.meta", + "when": "false" + }, { "command": "kdb.connect.via.dialog", "when": "false" @@ -621,12 +662,12 @@ "view/item/context": [ { "command": "kdb.connect", - "when": "view == kdb-servers && viewItem not in kdb.connected", + "when": "view == kdb-servers && viewItem not in kdb.connected && (viewItem in kdb.rootNodes || viewItem in kdb.insightsNodes)", "group": "connection@1" }, { "command": "kdb.active.connection", - "when": "view == kdb-servers && viewItem in kdb.connected && (viewItem in kdb.rootNodes || viewItem in kdb.insightsNodes)", + "when": "view == kdb-servers && viewItem in kdb.connected && (viewItem in kdb.rootNodes || viewItem in kdb.insightsNodes) && viewItem not in kdb.connected.active", "group": "connection@1" }, { @@ -639,6 +680,11 @@ "when": "view == kdb-servers && viewItem not in kdb.insightsNodes && viewItem in kdb.kdbNodesWithoutTls && viewItem not in kdb.local", "group": "connection@4" }, + { + "command": "kdb.insights.refreshMeta", + "when": "view == kdb-servers && viewItem in kdb.connected && viewItem in kdb.insightsNodes", + "group": "connection@3" + }, { "command": "kdb.insightsRemove", "when": "view == kdb-servers && viewItem in kdb.insightsNodes", @@ -692,8 +738,13 @@ "when": "editorLangId == q" }, { - "command": "kdb.terminal.run", + "command": "kdb.execute.block", "group": "q@2", + "when": "editorLangId == q" + }, + { + "command": "kdb.terminal.run", + "group": "q@3", "when": "editorLangId == q && !(resourceFilename =~ /.kdb.q/)" }, { @@ -719,8 +770,13 @@ "when": "editorLangId == q" }, { - "command": "kdb.terminal.run", + "command": "kdb.execute.block", "group": "q@2", + "when": "editorLangId == q" + }, + { + "command": "kdb.terminal.run", + "group": "q@3", "when": "editorLangId == q && !(resourceFilename =~ /.kdb.q/)" }, { diff --git a/resources/metaIcons/aggicon.svg b/resources/metaIcons/aggicon.svg new file mode 100644 index 00000000..4834b95f --- /dev/null +++ b/resources/metaIcons/aggicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/metaIcons/apiicon.svg b/resources/metaIcons/apiicon.svg new file mode 100644 index 00000000..2b71c797 --- /dev/null +++ b/resources/metaIcons/apiicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/metaIcons/dapicon.svg b/resources/metaIcons/dapicon.svg new file mode 100644 index 00000000..8da22fb1 --- /dev/null +++ b/resources/metaIcons/dapicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/metaIcons/metaicon.svg b/resources/metaIcons/metaicon.svg new file mode 100644 index 00000000..2b95a430 --- /dev/null +++ b/resources/metaIcons/metaicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/metaIcons/packageicon.svg b/resources/metaIcons/packageicon.svg new file mode 100644 index 00000000..ba7f543a --- /dev/null +++ b/resources/metaIcons/packageicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/metaIcons/rcicon.svg b/resources/metaIcons/rcicon.svg new file mode 100644 index 00000000..4d546120 --- /dev/null +++ b/resources/metaIcons/rcicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/metaIcons/schemaicon.svg b/resources/metaIcons/schemaicon.svg new file mode 100644 index 00000000..86d82b46 --- /dev/null +++ b/resources/metaIcons/schemaicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/server/src/qLangServer.ts b/server/src/qLangServer.ts index fd0ba8f1..0529e1e8 100644 --- a/server/src/qLangServer.ts +++ b/server/src/qLangServer.ts @@ -29,10 +29,13 @@ import { Range, ReferenceParams, RenameParams, + SelectionRange, + SelectionRangeParams, ServerCapabilities, SymbolKind, TextDocumentChangeEvent, TextDocumentIdentifier, + TextDocumentPositionParams, TextDocumentSyncKind, TextDocuments, TextEdit, @@ -54,6 +57,10 @@ import { namespace, relative, testblock, + EndOfLine, + SemiColon, + WhiteSpace, + RCurly, } from "./parser"; import { lint } from "./linter"; @@ -86,6 +93,15 @@ export default class QLangServer { this.connection.onDidChangeConfiguration( this.onDidChangeConfiguration.bind(this), ); + this.connection.onRequest( + "kdb.qls.expressionRange", + this.onExpressionRange.bind(this), + ); + this.connection.onRequest( + "kdb.qls.parameterCache", + this.onParameterCache.bind(this), + ); + this.connection.onSelectionRanges(this.onSelectionRanges.bind(this)); } public capabilities(): ServerCapabilities { @@ -96,6 +112,7 @@ export default class QLangServer { definitionProvider: true, renameProvider: true, completionProvider: { resolveProvider: false }, + selectionRangeProvider: true, }; } @@ -213,6 +230,76 @@ export default class QLangServer { }); } + public onExpressionRange({ + textDocument, + position, + }: TextDocumentPositionParams) { + const tokens = this.parse(textDocument); + const source = positionToToken(tokens, position); + if (!source || !source.exprs) { + return null; + } + return expressionToRange(tokens, source.exprs); + } + + public onParameterCache({ + textDocument, + position, + }: TextDocumentPositionParams) { + const tokens = this.parse(textDocument); + const source = positionToToken(tokens, position); + if (!source) { + return null; + } + const lambda = inLambda(source); + if (!lambda) { + return null; + } + const scoped = tokens.filter((token) => inLambda(token) === lambda); + if (scoped.length === 0) { + return null; + } + const curly = scoped[scoped.length - 1]; + if (!curly || curly.tokenType !== RCurly) { + return null; + } + const params = scoped.filter((token) => inParam(token)); + if (params.length === 0) { + return null; + } + const bracket = params[params.length - 1]; + if (!bracket) { + return null; + } + const args = params + .filter((token) => assigned(token)) + .map((token) => token.image); + if (args.length === 0) { + return null; + } + return { + params: args, + start: rangeFromToken(bracket).end, + end: rangeFromToken(curly).start, + }; + } + + public onSelectionRanges({ + textDocument, + positions, + }: SelectionRangeParams): SelectionRange[] { + const tokens = this.parse(textDocument); + const ranges: SelectionRange[] = []; + + for (const position of positions) { + const source = positionToToken(tokens, position); + if (source) { + ranges.push(SelectionRange.create(rangeFromToken(source))); + } + } + return ranges; + } + private parse(textDocument: TextDocumentIdentifier): Token[] { const document = this.documents.get(textDocument.uri); if (!document) { @@ -243,6 +330,25 @@ function positionToToken(tokens: Token[], position: Position) { }); } +function expressionToRange(tokens: Token[], expression: number) { + const exprs = tokens.filter( + (token) => + token.exprs === expression && + token.tokenType !== EndOfLine && + token.tokenType !== SemiColon && + token.tokenType !== WhiteSpace, + ); + const first = exprs[0]; + if (!first) { + return null; + } + const last = exprs[exprs.length - 1]; + const start = rangeFromToken(first); + const end = last ? rangeFromToken(last) : start; + + return Range.create(start.start, end.end); +} + function createSymbol(token: Token, tokens: Token[]): DocumentSymbol { const range = rangeFromToken(token); return DocumentSymbol.create( @@ -277,6 +383,8 @@ function createDebugSymbol(token: Token): DocumentSymbol { tokenId(token), `${token.tokenType.name} ${token.namespace ? `(${token.namespace})` : ""} ${ token.error !== undefined ? `E=${token.error}` : "" + } ${ + token.exprs ? `X=${token.exprs}` : "" } ${token.order ? `O=${token.order}` : ""} ${ token.tangled ? `T=${tokenId(token.tangled)}` : "" } ${token.scope ? `S=${tokenId(token.scope)}` : ""} ${ diff --git a/sonar-project.properties b/sonar-project.properties index dd25b38d..4a193f9f 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,4 +3,4 @@ sonar.qualitygate.wait=true sonar.sources=src,server sonar.tests=test sonar.javascript.lcov.reportPaths=lcov.info -sonar.coverage.exclusions=server/src/utils/parserUtils.ts,src/ipc/**,src/models/**,src/extension.ts,src/classes/** +sonar.coverage.exclusions=server/src/utils/parserUtils.ts,src/ipc/**,src/models/**,src/extension.ts,src/classes/**,src/commands/installTools.ts,src/utils/cpUtils.ts diff --git a/src/classes/insightsConnection.ts b/src/classes/insightsConnection.ts index 06582e14..bf60ef30 100644 --- a/src/classes/insightsConnection.ts +++ b/src/classes/insightsConnection.ts @@ -15,7 +15,7 @@ import { ext } from "../extensionVariables"; import axios, { AxiosRequestConfig } from "axios"; import { ProgressLocation, window } from "vscode"; import * as url from "url"; -import { MetaObject, MetaObjectPayload } from "../models/meta"; +import { MetaInfoType, MetaObject, MetaObjectPayload } from "../models/meta"; import { getCurrentToken } from "../services/kdbInsights/codeFlowLogin"; import { InsightsNode } from "../services/kdbTreeProvider"; import { GetDataObjectPayload } from "../models/data"; @@ -25,12 +25,22 @@ import { jwtDecode } from "jwt-decode"; import { JwtUser } from "../models/jwt_user"; import { Telemetry } from "../utils/telemetryClient"; import { handleScratchpadTableRes, handleWSResults } from "../utils/queryUtils"; +import { + invalidUsernameJWT, + kdbOutputLog, + tokenUndefinedError, +} from "../utils/core"; +import { InsightsConfig, InsightsEndpoints } from "../models/config"; +import { convertTimeToTimestamp } from "../utils/dataSource"; export class InsightsConnection { public connected: boolean; public connLabel: string; public node: InsightsNode; public meta?: MetaObject; + public config?: InsightsConfig; + public insightsVersion?: string; + public connEndpoints?: InsightsEndpoints; constructor(connLabel: string, node: InsightsNode) { this.connected = false; @@ -43,9 +53,11 @@ export class InsightsConnection { await getCurrentToken( this.node.details.server, this.node.details.alias, + this.node.details.realm || "insights", ).then(async (token) => { this.connected = token ? true : false; if (token) { + await this.getConfig(); await this.getMeta(); } }); @@ -72,13 +84,11 @@ export class InsightsConnection { const token = await getCurrentToken( this.node.details.server, this.node.details.alias, + this.node.details.realm || "insights", ); if (token === undefined) { - ext.outputChannel.appendLine( - "Error retrieving access token for insights.", - ); - window.showErrorMessage("Failed to retrieve access token for insights"); + tokenUndefinedError(this.connLabel); return undefined; } @@ -94,6 +104,98 @@ export class InsightsConnection { return undefined; } + public async getConfig() { + if (this.connected) { + const configUrl = new url.URL( + ext.insightsAuthUrls.configURL, + this.node.details.server, + ); + const token = await getCurrentToken( + this.node.details.server, + this.node.details.alias, + this.node.details.realm || "insights", + ); + + if (token === undefined) { + tokenUndefinedError(this.connLabel); + return undefined; + } + + const options = { + headers: { Authorization: `Bearer ${token.accessToken}` }, + }; + + const configResponse = await axios.get(configUrl.toString(), options); + this.config = configResponse.data; + this.getInsightsVersion(); + this.defineEndpoints(); + } + } + + public getInsightsVersion() { + const match = this.config?.version.match(/-\d+(\.\d+){2}(-|$)/); + const version = match ? match[0].replace(/-/g, "") : null; + if (version) { + const [major, minor, _path] = version.split("."); + this.insightsVersion = `${major}.${minor}`; + } + } + + public defineEndpoints() { + if (this.insightsVersion) { + switch (this.insightsVersion) { + // uncomment it when SCRATCHPAD merge to Insights + // case "1.11": + // this.connEndpoints = { + // scratchpad: { + // scratchpad: "scratchpad-manager/api/v1/execute/display", + // import: "scratchpad-manager/api/v1/execute/import/data", + // importSql: "scratchpad-manager/api/v1/execute/import/sql", + // importQsql: "scratchpad-manager/api/v1/execute/import/qsql", + // reset: "scratchpad-manager/api/v1/execute/reset", + // }, + // serviceGateway: { + // meta: "servicegateway/meta", + // data: "servicegateway/data", + // sql: "servicegateway/qe/sql", + // qsql: "servicegateway/qe/qsql", + // }, + // }; + // break; + default: + this.connEndpoints = { + scratchpad: { + scratchpad: "servicebroker/scratchpad/display", + import: "servicebroker/scratchpad/import/data", + importSql: "servicebroker/scratchpad/import/sql", + importQsql: "servicebroker/scratchpad/import/qsql", + reset: "servicebroker/scratchpad/reset", + }, + serviceGateway: { + meta: "servicegateway/meta", + data: "servicegateway/data", + sql: "servicegateway/qe/sql", + qsql: "servicegateway/qe/qsql", + }, + }; + break; + } + } + } + + public retrieveEndpoints( + parentKey: "scratchpad" | "serviceGateway", + childKey: string, + ): string | undefined { + if (this.connEndpoints) { + const parent = this.connEndpoints[parentKey]; + if (parent) { + return parent[childKey as keyof typeof parent]; + } + return undefined; + } + } + public async getDataInsights( targetUrl: string, body: string, @@ -106,16 +208,10 @@ export class InsightsConnection { const token = await getCurrentToken( this.node.details.server, this.node.details.alias, + this.node.details.realm || "insights", ); if (token === undefined) { - ext.outputChannel.appendLine( - "Error retrieving access token for insights connection named: " + - this.connLabel, - ); - window.showErrorMessage( - "Failed to retrieve access token for insights connection named: " + - this.connLabel, - ); + tokenUndefinedError(this.connLabel); return undefined; } const headers = { @@ -138,15 +234,16 @@ export class InsightsConnection { }, async (progress, token) => { token.onCancellationRequested(() => { - ext.outputChannel.appendLine("User cancelled the execution."); + kdbOutputLog(`User cancelled the Datasource Run.`, "WARNING"); }); progress.report({ message: "Query executing..." }); return await axios(options) .then((response: any) => { - ext.outputChannel.appendLine( - `request status: ${response.status}`, + kdbOutputLog( + `[Datasource RUN] Status: ${response.status}.`, + "INFO", ); if (isCompressed(response.data)) { response.data = uncompress(response.data); @@ -159,8 +256,9 @@ export class InsightsConnection { }; }) .catch((error: any) => { - ext.outputChannel.appendLine( - `request status: ${error.response.status}`, + kdbOutputLog( + `[Datasource RUN] Status: ${error.response.status}.`, + "INFO", ); return { error: { buffer: error.response.data }, @@ -179,21 +277,21 @@ export class InsightsConnection { params: DataSourceFiles, ): Promise { let dsTypeString = ""; - if (this.connected) { + if (this.connected && this.connEndpoints) { let queryParams, coreUrl: string; switch (params.dataSource.selectedType) { case DataSourceTypes.API: queryParams = { table: params.dataSource.api.table, - startTS: params.dataSource.api.startTS, - endTS: params.dataSource.api.endTS, + startTS: convertTimeToTimestamp(params.dataSource.api.startTS), + endTS: convertTimeToTimestamp(params.dataSource.api.endTS), }; - coreUrl = ext.insightsScratchpadUrls.import; + coreUrl = this.connEndpoints.scratchpad.import; dsTypeString = "API"; break; case DataSourceTypes.SQL: queryParams = { query: params.dataSource.sql.query }; - coreUrl = ext.insightsScratchpadUrls.importSql; + coreUrl = this.connEndpoints.scratchpad.importSql; dsTypeString = "SQL"; break; case DataSourceTypes.QSQL: @@ -204,7 +302,7 @@ export class InsightsConnection { target: assemblyParts[1], query: params.dataSource.qsql.query, }; - coreUrl = ext.insightsScratchpadUrls.importQsql; + coreUrl = this.connEndpoints.scratchpad.importQsql; dsTypeString = "QSQL"; break; default: @@ -216,21 +314,17 @@ export class InsightsConnection { const token = await getCurrentToken( this.node.details.server, this.node.details.alias, + this.node.details.realm || "insights", ); if (token === undefined) { - ext.outputChannel.appendLine( - "Error retrieving access token for insights.", - ); - window.showErrorMessage("Failed to retrieve access token for insights"); + tokenUndefinedError(this.connLabel); return undefined; } const username = jwtDecode(token.accessToken); if (username === undefined || username.preferred_username === "") { - ext.outputChannel.appendLine( - "JWT did not contain a valid preferred username", - ); + invalidUsernameJWT(this.connLabel); } const headers = { headers: { @@ -251,9 +345,7 @@ export class InsightsConnection { }, async (progress, token) => { token.onCancellationRequested(() => { - ext.outputChannel.appendLine( - "User cancelled the scratchpad import.", - ); + kdbOutputLog(`User cancelled the scratchpad import.`, "WARNING"); }); progress.report({ message: "Populating scratchpad..." }); @@ -264,9 +356,20 @@ export class InsightsConnection { headers, ); - ext.outputChannel.append(JSON.stringify(scratchpadResponse.data)); + kdbOutputLog( + `Executed successfully, stored in ${variableName}.`, + "INFO", + ); + kdbOutputLog( + `[SCRATCHPAD] Status: ${scratchpadResponse.status}`, + "INFO", + ); + kdbOutputLog( + `[SCRATCHPAD] Populated scratchpad with the following params: ${JSON.stringify(body.params)}`, + "INFO", + ); window.showInformationMessage( - `Executed successfully, stored in ${variableName}`, + `Executed successfully, stored in ${variableName}.`, ); Telemetry.sendEvent( "Datasource." + dsTypeString + ".Scratchpad.Populated", @@ -276,6 +379,8 @@ export class InsightsConnection { return p; }, ); + } else { + this.noConnectionOrEndpoints(); } } @@ -284,28 +389,24 @@ export class InsightsConnection { context?: string, isPython?: boolean, ): Promise { - if (this.connected) { - const isTableView = ext.resultsViewProvider.isVisible(); + if (this.connected && this.connEndpoints) { + const isTableView = ext.isResultsTabVisible; const scratchpadURL = new url.URL( - ext.insightsAuthUrls.scratchpadURL, + this.connEndpoints.scratchpad.scratchpad, this.node.details.server, ); const token = await getCurrentToken( this.node.details.server, this.node.details.alias, + this.node.details.realm || "insights", ); if (token === undefined) { - ext.outputChannel.appendLine( - "Error retrieving access token for insights.", - ); - window.showErrorMessage("Failed to retrieve access token for insights"); + tokenUndefinedError(this.connLabel); return undefined; } const username = jwtDecode(token.accessToken); if (username === undefined || username.preferred_username === "") { - ext.outputChannel.appendLine( - "JWT did not contain a valid preferred username", - ); + invalidUsernameJWT(this.connLabel); } const body = { expression: query, @@ -328,15 +429,14 @@ export class InsightsConnection { }, async (progress, token) => { token.onCancellationRequested(() => { - ext.outputChannel.appendLine( - "User cancelled the scratchpad execution.", - ); + kdbOutputLog(`User cancelled the Scrathpad execution.`, "WARNING"); }); progress.report({ message: "Query is executing..." }); const spRes = await axios .post(scratchpadURL.toString(), body, { headers }) .then((response: any) => { + kdbOutputLog(`[SCRATCHPAD] Status: ${response.status}`, "INFO"); if (isTableView && !response.data.error) { const buffer = new Uint8Array( response.data.data.map((x: string) => parseInt(x, 16)), @@ -353,35 +453,33 @@ export class InsightsConnection { }, ); return spReponse; + } else { + this.noConnectionOrEndpoints(); } return undefined; } public async resetScratchpad(): Promise { - if (this.connected) { + if (this.connected && this.connEndpoints) { const scratchpadURL = new url.URL( - ext.insightsScratchpadUrls.reset!, + this.connEndpoints.scratchpad.reset, this.node.details.server, ); const token = await getCurrentToken( this.node.details.server, this.node.details.alias, + this.node.details.realm || "insights", ); if (token === undefined) { - ext.outputChannel.appendLine( - "Error retrieving access token for insights.", - ); - window.showErrorMessage("Failed to retrieve access token for insights"); + tokenUndefinedError(this.connLabel); return false; } const username = jwtDecode(token.accessToken); if (username === undefined || username.preferred_username === "") { - ext.outputChannel.appendLine( - "JWT did not contain a valid preferred username", - ); + invalidUsernameJWT(this.connLabel); return false; } const headers = { @@ -398,27 +496,28 @@ export class InsightsConnection { }, async (progress, token) => { token.onCancellationRequested(() => { - ext.outputChannel.appendLine( - "User cancelled the scratchpad reset.", - ); + kdbOutputLog(`User cancelled the scratchpad reset.`, "WARNING"); return false; }); - progress.report({ message: "Reseting scratchpad..." }); - const res = await axios .post(scratchpadURL.toString(), null, headers) - .then((response: any) => { - console.log(response); - ext.outputChannel.append("Scratchpad.Reseted"); + .then((_response: any) => { + kdbOutputLog( + `[SCRATCHPAD] Executed successfully, scratchpad reseted at ${this.connLabel} connection.`, + "INFO", + ); window.showInformationMessage( `Executed successfully, scratchpad reseted at ${this.connLabel} connection`, ); Telemetry.sendEvent("Scratchpad.Reseted"); return true; }) - .catch((error: any) => { - console.log(error); + .catch((_error: any) => { + kdbOutputLog( + `[SCRATCHPAD] Error ocurried while reseting scratchpad in connection ${this.connLabel}, try again.`, + "ERROR", + ); window.showErrorMessage( "Error ocurried while reseting scratchpad, try again.", ); @@ -429,60 +528,53 @@ export class InsightsConnection { }, ); } else { + this.noConnectionOrEndpoints(); return false; } } - public async pingInsights(): Promise { - if (this.connected) { - const pingURL = new url.URL( - ext.insightsServiceGatewayUrls.ping, - this.node.details.server, - ); - - const userToken = await getCurrentToken( - this.node.details.server, - this.node.details.alias, + public returnMetaObject(metaType: MetaInfoType): string { + if (!this.meta) { + kdbOutputLog( + `Meta data is undefined for connection ${this.connLabel}`, + "ERROR", ); + return ""; + } - if (userToken === undefined) { - ext.outputChannel.appendLine( - "Error retrieving access token for insights.", - ); - window.showErrorMessage("Failed to retrieve access token for insights"); - return false; - } + let objectToReturn; + + switch (metaType) { + case MetaInfoType.META: + objectToReturn = this.meta.payload; + break; + case MetaInfoType.SCHEMA: + objectToReturn = this.meta.payload.schema; + break; + case MetaInfoType.API: + objectToReturn = this.meta.payload.api; + break; + case MetaInfoType.AGG: + objectToReturn = this.meta.payload.agg; + break; + case MetaInfoType.DAP: + objectToReturn = this.meta.payload.dap; + break; + case MetaInfoType.RC: + objectToReturn = this.meta.payload.rc; + break; + default: + kdbOutputLog(`Invalid meta type: ${metaType}`, "ERROR"); + return ""; + } - const body = { - labels: {}, - }; - const startTime = Date.now(); - - return await axios - .request({ - method: "post", - url: pingURL.toString(), - data: body, - headers: { Authorization: `Bearer ${userToken.accessToken}` }, - timeout: 2000, - }) - .then((_response: any) => { - Telemetry.sendEvent("Insights.Pinged"); - return true; - }) - .catch((_error: any) => { - const endTime = Date.now(); - const timeString = new Date().toLocaleTimeString(); - const timeDiff = endTime - startTime; - ext.outputChannel.appendLine( - `[${timeString}] Connection keep alive error: ${this.connLabel}. Ping failed. ${_error.code}: status code ${_error.response.status}. Time Elapsed ${timeDiff}ms`, - ); + return JSON.stringify(objectToReturn); + } - window.showErrorMessage( - `Error in connection: ${this.connLabel}, check kdb OUTPUT for more info.`, - ); - return false; - }); - } + public noConnectionOrEndpoints(): void { + kdbOutputLog( + `No connection or endpoints defined for ${this.connLabel}`, + "ERROR", + ); } } diff --git a/src/classes/localConnection.ts b/src/classes/localConnection.ts index e56c5268..ae6469fc 100644 --- a/src/classes/localConnection.ts +++ b/src/classes/localConnection.ts @@ -14,7 +14,7 @@ import * as nodeq from "node-q"; import { commands, window } from "vscode"; import { ext } from "../extensionVariables"; -import { delay } from "../utils/core"; +import { delay, kdbOutputLog } from "../utils/core"; import { convertStringToArray, handleQueryResults } from "../utils/execution"; import { queryWrapper } from "../utils/queryUtils"; import { QueryResult, QueryResultType } from "../models/queryResult"; @@ -79,15 +79,17 @@ export class LocalConnection { window.showErrorMessage( `Connection to server ${this.options.host}:${this.options.port} failed! Details: ${err?.message}`, ); - ext.outputChannel.appendLine( + kdbOutputLog( `Connection to server ${this.options.host}:${this.options.port} failed! Details: ${err?.message}`, + "ERROR", ); return; } conn.addListener("close", () => { commands.executeCommand("kdb.disconnect", this.connLabel); - ext.outputChannel.appendLine( - `Connection stopped from ${this.options.host}:${this.options.port}`, + kdbOutputLog( + `Connection closed: ${this.options.host}:${this.options.port}`, + "INFO", ); ext.outputChannel.show(); }); @@ -182,10 +184,12 @@ export class LocalConnection { ); while (result === undefined || result === null) { - await delay(500); + await delay(50); } - if (ext.resultsViewProvider.isVisible() && stringify) { + this.updateGlobal(); + + if (ext.isResultsTabVisible && stringify) { if (this.isError) { this.isError = false; return result; diff --git a/src/commands/clientCommands.ts b/src/commands/clientCommands.ts new file mode 100644 index 00000000..475e56cc --- /dev/null +++ b/src/commands/clientCommands.ts @@ -0,0 +1,133 @@ +/* + * Copyright (c) 1998-2023 Kx Systems Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +import { + EndOfLine, + ExtensionContext, + Position, + Range, + Selection, + WorkspaceEdit, + commands, + workspace, +} from "vscode"; +import { LanguageClient } from "vscode-languageclient/node"; +import { ext } from "../extensionVariables"; +import { runActiveEditor } from "./workspaceCommand"; +import { ExecutionTypes } from "../models/execution"; +import crypto from "crypto"; + +async function executeBlock(client: LanguageClient) { + if (ext.activeTextEditor) { + const range = await client.sendRequest("kdb.qls.expressionRange", { + textDocument: { uri: `${ext.activeTextEditor.document.uri}` }, + position: ext.activeTextEditor.selection.active, + }); + if (range) { + ext.activeTextEditor.selection = new Selection( + range.start.line, + range.start.character, + range.end.line, + range.end.character, + ); + await runActiveEditor(ExecutionTypes.QuerySelection); + } + } +} + +async function toggleParameterCache(client: LanguageClient) { + if (ext.activeTextEditor) { + const doc = ext.activeTextEditor.document; + const res = await client.sendRequest<{ + params: string[]; + start: Position; + end: Position; + }>("kdb.qls.parameterCache", { + textDocument: { uri: `${doc.uri}` }, + position: ext.activeTextEditor.selection.active, + }); + if (res) { + const edit = new WorkspaceEdit(); + const start = new Position(res.start.line, res.start.character); + const end = new Position(res.end.line, res.end.character); + const text = doc.getText(new Range(start, end)); + const match = + /\s*\.axdebug\.temp([A-F0-9]{6}).*?\.axdebug\.temp\1\s*;/s.exec(text); + if (match) { + const offset = doc.offsetAt(start); + edit.delete( + doc.uri, + new Range( + doc.positionAt(offset + match.index), + doc.positionAt(offset + match.index + match[0].length), + ), + ); + } else { + const hash = crypto.randomBytes(3).toString("hex").toUpperCase(); + const expr1 = `.axdebug.temp${hash}: ${res.params.length === 1 ? res.params[0] : `(${res.params.join(";")})`};`; + const expr2 = `${res.params.map((param) => `\`${param}`).join("")} set${res.params.length === 1 ? "" : "'"} .axdebug.temp${hash};`; + + if (start.line === end.line) { + edit.insert(doc.uri, start, " "); + edit.insert(doc.uri, start, expr1); + edit.insert(doc.uri, start, expr2); + } else { + const line = doc.getText( + new Range(end.line, 0, end.line, end.character), + ); + const match = /^[ \t]*/.exec(line); + if (match) { + const eol = doc.eol === EndOfLine.CRLF ? "\r\n" : "\n"; + edit.insert(doc.uri, start, eol); + edit.insert(doc.uri, start, match[0]); + edit.insert(doc.uri, start, expr1); + edit.insert(doc.uri, start, eol); + edit.insert(doc.uri, start, match[0]); + edit.insert(doc.uri, start, expr2); + } + } + } + await workspace.applyEdit(edit); + } + } +} + +export function connectClientCommands( + context: ExtensionContext, + client: LanguageClient, +) { + let mutex = false; + + context.subscriptions.push( + commands.registerCommand("kdb.execute.block", async () => { + if (!mutex) { + mutex = true; + try { + await executeBlock(client); + } finally { + mutex = false; + } + } + }), + commands.registerCommand("kdb.toggleParameterCache", async () => { + if (!mutex) { + mutex = true; + try { + await toggleParameterCache(client); + } finally { + mutex = false; + } + } + }), + ); +} diff --git a/src/commands/dataSourceCommand.ts b/src/commands/dataSourceCommand.ts index 24196eae..f8461ebb 100644 --- a/src/commands/dataSourceCommand.ts +++ b/src/commands/dataSourceCommand.ts @@ -45,7 +45,7 @@ import { Telemetry } from "../utils/telemetryClient"; import { LocalConnection } from "../classes/localConnection"; import { ConnectionManagementService } from "../services/connectionManagerService"; import { InsightsConnection } from "../classes/insightsConnection"; -import { offerConnectAction } from "../utils/core"; +import { kdbOutputLog, offerConnectAction } from "../utils/core"; export async function addDataSource(): Promise { const kdbDataSourcesFolderPath = createKdbDataSourcesFolder(); @@ -102,8 +102,9 @@ export async function populateScratchpad( dataSourceForm!, ); } else { - ext.outputChannel.appendLine( - `Invalid scratchpad output variable name: ${outputVariable}`, + kdbOutputLog( + `[DATASOURCE] Invalid scratchpad output variable name: ${outputVariable}`, + "ERROR", ); } }); @@ -135,7 +136,10 @@ export async function runDataSource( dataSourceForm.insightsNode = getConnectedInsightsNode(); const fileContent = dataSourceForm; - ext.outputChannel.appendLine(`Running ${fileContent.name} datasource...`); + kdbOutputLog( + `[DATASOURCE] Running ${fileContent.name} datasource...`, + "INFO", + ); let res: any; const selectedType = getSelectedType(fileContent); ext.isDatasourceExecution = true; @@ -160,9 +164,9 @@ export async function runDataSource( if (!success) { window.showErrorMessage(res.error); - } else if (ext.resultsViewProvider.isVisible()) { + } else if (ext.isResultsTabVisible) { const resultCount = typeof res === "string" ? "0" : res.rows.length; - ext.outputChannel.appendLine(`Results: ${resultCount} rows`); + kdbOutputLog(`[DATASOURCE] Results: ${resultCount} rows`, "INFO"); writeQueryResultsToView( res, query, @@ -172,8 +176,9 @@ export async function runDataSource( selectedType, ); } else { - ext.outputChannel.appendLine( - `Results is a string with length: ${res.length}`, + kdbOutputLog( + `[DATASOURCE] Results is a string with length: ${res.length}`, + "INFO", ); writeQueryResultsToConsole( res, @@ -188,8 +193,8 @@ export async function runDataSource( } } catch (error) { window.showErrorMessage((error as Error).message); + kdbOutputLog(`[DATASOURCE] ${(error as Error).message}`, "ERROR"); DataSourcesPanel.running = false; - //TODO ADD ERROR TO CONSOLE HERE } finally { DataSourcesPanel.running = false; } @@ -397,11 +402,11 @@ export function getQuery( } } -function parseError(error: GetDataError) { +export function parseError(error: GetDataError) { if (error instanceof Object && error.buffer) { return handleWSError(error.buffer); } else { - ext.outputChannel.appendLine(`Error: ${error}`); + kdbOutputLog(`[DATASOURCE] Error: ${error}`, "ERROR"); return { error, }; diff --git a/src/commands/installTools.ts b/src/commands/installTools.ts index f81fcb49..4ab794ae 100644 --- a/src/commands/installTools.ts +++ b/src/commands/installTools.ts @@ -50,11 +50,12 @@ import { addLocalConnectionStatus, convertBase64License, delay, - getHash, + getKeyForServerName, getOsFile, getServerName, getServers, getWorkspaceFolder, + kdbOutputLog, removeLocalConnectionStatus, saveLocalProcessObj, updateServers, @@ -84,7 +85,7 @@ export async function installTools(): Promise { .showInformationMessage( licenseWorkflow.prompt, licenseWorkflow.option1, - licenseWorkflow.option2 + licenseWorkflow.option2, ) .then(async (res) => { if (res === licenseWorkflow.option2) { @@ -99,7 +100,7 @@ export async function installTools(): Promise { { placeHolder: licenseTypePlaceholder, ignoreFocusOut: true, - } + }, ); if (licenseResult === undefined) { @@ -138,7 +139,7 @@ export async function installTools(): Promise { }, async (progress, token) => { token.onCancellationRequested(() => { - ext.outputChannel.appendLine("User cancelled the installation."); + kdbOutputLog("[Install] User cancelled the installation.", "INFO"); }); progress.report({ increment: 0 }); @@ -147,29 +148,31 @@ export async function installTools(): Promise { progress.report({ increment: 20, message: "Getting the binaries..." }); const osFile = getOsFile(); if (osFile === undefined) { - ext.outputChannel.appendLine( - "Unsupported operating system, unable to download binaries for this." + kdbOutputLog( + "[Install] Unsupported operating system, unable to download binaries for this.", + "ERROR", ); Telemetry.sendException( new Error( - "Unsupported operating system, unable to download binaries" - ) + "Unsupported operating system, unable to download binaries", + ), ); } else { const gpath = join(ext.context.globalStorageUri.fsPath, osFile); if (!existsSync(gpath)) { const response = await fetch( - `${ext.kdbDownloadPrefixUrl}${osFile}` + `${ext.kdbDownloadPrefixUrl}${osFile}`, ); if (response.status > 200) { Telemetry.sendException( - new Error("Invalid or unavailable download url.") + new Error("Invalid or unavailable download url."), ); - ext.outputChannel.appendLine( - `Invalid or unavailable download url: ${runtimeUrl}` + kdbOutputLog( + `[Install] Invalid or unavailable download url: ${runtimeUrl}`, + "ERROR", ); window.showErrorMessage( - `Invalid or unavailable download url: ${runtimeUrl}` + `Invalid or unavailable download url: ${runtimeUrl}`, ); exit(1); } @@ -185,7 +188,7 @@ export async function installTools(): Promise { await ensureDir(ext.context.globalStorageUri.fsPath); await copy( file![0].fsPath, - join(ext.context.globalStorageUri.fsPath, ext.kdbLicName) + join(ext.context.globalStorageUri.fsPath, ext.kdbLicName), ); // add the env var for the process @@ -208,24 +211,25 @@ export async function installTools(): Promise { if (QHOME) { env.QHOME = QHOME; if (!pathExists(env.QHOME)) { - ext.outputChannel.appendLine("QHOME path stored is empty"); + kdbOutputLog("[Install] QHOME path stored is empty", "ERROR"); } await writeFile( join(__dirname, "qinstall.md"), - `# q runtime installed location: \n### ${QHOME}` + `# q runtime installed location: \n### ${QHOME}`, ); - ext.outputChannel.appendLine( - `Installation of q found here: ${QHOME}` + kdbOutputLog( + `[Install] Installation of q found here: ${QHOME}`, + "INFO", ); } - } + }, ) .then(async () => { window .showInformationMessage( onboardingWorkflow.prompt(ext.context.globalStorageUri.fsPath), onboardingWorkflow.option1, - onboardingWorkflow.option2 + onboardingWorkflow.option2, ) .then(async (startResult) => { if (startResult === onboardingWorkflow.option1) { @@ -241,16 +245,16 @@ export async function installTools(): Promise { let servers: Server | undefined = getServers(); if ( servers != undefined && - servers[getHash(`localhost:${port}`)] + servers[getKeyForServerName("local")] ) { Telemetry.sendEvent( - `Server localhost:${port} already exists in configuration store.` + `Server localhost:${port} already exists in configuration store.`, ); await window.showErrorMessage( - `Server localhost:${port} already exists.` + `Server localhost:${port} already exists.`, ); } else { - const key = getHash(`localhost${port}local`); + const key = "local"; if (servers === undefined) { servers = { key: { @@ -273,7 +277,7 @@ export async function installTools(): Promise { tls: false, }; await addLocalConnectionContexts( - getServerName(servers[key]) + getServerName(servers[key]), ); } await updateServers(servers); @@ -285,8 +289,8 @@ export async function installTools(): Promise { } await startLocalProcessByServerName( `localhost:${port} [local]`, - getHash(`localhost${port}local`), - Number(port) + getKeyForServerName("local"), + Number(port), ); }); } @@ -297,10 +301,10 @@ export async function installTools(): Promise { export async function startLocalProcessByServerName( serverName: string, index: string, - port: number + port: number, ): Promise { const workingDirectory = await getWorkspaceFolder( - workspace.getConfiguration().get("kdb.qHomeDirectory")! + workspace.getConfiguration().get("kdb.qHomeDirectory")!, ); await addLocalConnectionStatus(serverName); @@ -310,13 +314,13 @@ export async function startLocalProcessByServerName( saveLocalProcessObj, "-p", port.toString(), - index + index, ); } export async function startLocalProcess(viewItem: KdbNode): Promise { const workingDirectory = await getWorkspaceFolder( - workspace.getConfiguration().get("kdb.qHomeDirectory")! + workspace.getConfiguration().get("kdb.qHomeDirectory")!, ); await addLocalConnectionStatus(`${getServerName(viewItem.details)}`); @@ -326,27 +330,28 @@ export async function startLocalProcess(viewItem: KdbNode): Promise { saveLocalProcessObj, "-p", viewItem.details.serverPort.toString(), - viewItem.children[0] + viewItem.children[0], ); } export async function stopLocalProcess(viewItem: KdbNode): Promise { ext.localProcessObjects[viewItem.children[0]].kill(); window.showInformationMessage("q process stopped successfully!"); - ext.outputChannel.appendLine( + kdbOutputLog( `Child process id ${ext.localProcessObjects[viewItem.children[0]] - .pid!} removed in cache.` + .pid!} removed in cache.`, + "INFO", ); await removeLocalConnectionStatus(`${getServerName(viewItem.details)}`); } export async function stopLocalProcessByServerName( - serverName: string + serverName: string, ): Promise { ext.localProcessObjects[serverName].kill(); window.showInformationMessage("q process stopped successfully!"); - ext.outputChannel.appendLine( - `Child process id ${ext.localProcessObjects[serverName] - .pid!} removed in cache.` + kdbOutputLog( + `Child process id ${ext.localProcessObjects[serverName].pid!} removed in cache.`, + "INFO", ); } diff --git a/src/commands/serverCommand.ts b/src/commands/serverCommand.ts index 6f642921..19ead07e 100644 --- a/src/commands/serverCommand.ts +++ b/src/commands/serverCommand.ts @@ -14,7 +14,15 @@ import { readFileSync } from "fs-extra"; import { join } from "path"; import * as url from "url"; -import { Position, Range, commands, window } from "vscode"; +import { + Position, + Range, + Uri, + ViewColumn, + commands, + window, + workspace, +} from "vscode"; import { ext } from "../extensionVariables"; import { DataSourceFiles } from "../models/dataSource"; import { ExecutionTypes } from "../models/execution"; @@ -24,14 +32,20 @@ import { ScratchpadResult } from "../models/scratchpadResult"; import { Server, ServerDetails, ServerType } from "../models/server"; import { ServerObject } from "../models/serverObject"; import { DataSourcesPanel } from "../panels/datasource"; -import { InsightsNode, KdbNode } from "../services/kdbTreeProvider"; +import { + InsightsMetaNode, + InsightsNode, + KdbNode, + MetaObjectPayloadNode, +} from "../services/kdbTreeProvider"; import { addLocalConnectionContexts, checkOpenSslInstalled, - getHash, getInsights, + getKeyForServerName, getServerName, getServers, + kdbOutputLog, updateInsights, updateServers, } from "../utils/core"; @@ -52,6 +66,7 @@ import { NewConnectionPannel } from "../panels/newConnection"; import { Telemetry } from "../utils/telemetryClient"; import { ConnectionManagementService } from "../services/connectionManagerService"; import { InsightsConnection } from "../classes/insightsConnection"; +import { MetaContentProvider } from "../services/metaContentProvider"; export async function addNewConnection(): Promise { NewConnectionPannel.render(ext.context.extensionUri); @@ -69,19 +84,23 @@ export async function addInsightsConnection(insightsData: InsightDetails) { } let insights: Insights | undefined = getInsights(); - if (insights != undefined && insights[getHash(insightsData.server!)]) { + if ( + insights != undefined && + insights[getKeyForServerName(insightsData.alias)] + ) { await window.showErrorMessage( `Insights instance named ${insightsData.alias} already exists.`, ); return; } else { - const key = getHash(insightsData.server!); + const key = insightsData.alias; if (insights === undefined) { insights = { key: { auth: true, alias: insightsData.alias, server: insightsData.server!, + realm: insightsData.realm, }, }; } else { @@ -89,6 +108,7 @@ export async function addInsightsConnection(insightsData: InsightDetails) { auth: true, alias: insightsData.alias, server: insightsData.server!, + realm: insightsData.realm, }; } @@ -197,18 +217,13 @@ export async function addKdbConnection( if ( servers != undefined && - servers[getHash(`${kdbData.serverName}:${kdbData.serverPort}`)] + servers[getKeyForServerName(kdbData.serverAlias || "")] ) { await window.showErrorMessage( - `Server ${kdbData.serverName}:${kdbData.serverPort} already exists.`, + `Server name ${kdbData.serverAlias} already exists.`, ); } else { - const key = - kdbData.serverAlias != undefined - ? getHash( - `${kdbData.serverName}${kdbData.serverPort}${kdbData.serverAlias}`, - ) - : getHash(`${kdbData.serverName}${kdbData.serverPort}`); + const key = kdbData.serverAlias || ""; if (servers === undefined) { servers = { key: { @@ -310,6 +325,15 @@ export async function resetScratchPad(): Promise { await connMngService.resetScratchpad(); } +export async function refreshGetMeta(connLabel?: string): Promise { + const connMngService = new ConnectionManagementService(); + if (connLabel) { + await connMngService.refreshGetMeta(connLabel); + } else { + await connMngService.refreshAllGetMetas(); + } +} + export async function disconnect(connLabel: string): Promise { const connMngService = new ConnectionManagementService(); connMngService.disconnect(connLabel); @@ -337,7 +361,10 @@ export async function executeQuery( window.showErrorMessage( "No active connection found. Connect to one connection.", ); - //TODO ADD ERROR TO CONSOLE HERE + kdbOutputLog( + "No active connection found. Connect to one connection.", + "ERROR", + ); return undefined; } else { connLabel = ext.activeConnection.connLabel; @@ -346,7 +373,7 @@ export async function executeQuery( const isConnected = connMngService.isConnected(connLabel); if (!isConnected) { window.showInformationMessage("The selected connection is not connected."); - //TODO ADD ERROR TO CONSOLE HERE + kdbOutputLog("The selected connection is not connected.", "ERROR"); return undefined; } @@ -366,7 +393,7 @@ export async function executeQuery( ); return undefined; } - const isStringfy = !ext.resultsViewProvider.isVisible(); + const isStringfy = !ext.isResultsTabVisible; const startTime = Date.now(); const results = await connMngService.executeQuery( query, @@ -390,7 +417,8 @@ export async function executeQuery( duration, ); } else { - if (ext.resultsViewProvider.isVisible()) { + /* istanbul ignore next */ + if (ext.isResultsTabVisible) { writeQueryResultsToView( results, query, @@ -559,6 +587,25 @@ export async function loadServerObjects(): Promise { } } +export async function openMeta(node: MetaObjectPayloadNode | InsightsMetaNode) { + const metaContentProvider = new MetaContentProvider(); + workspace.registerTextDocumentContentProvider("meta", metaContentProvider); + const connMngService = new ConnectionManagementService(); + const doc = connMngService.retrieveMetaContent(node.connLabel, node.label); + if (doc && doc !== "") { + const formattedDoc = JSON.stringify(JSON.parse(doc), null, 2); + const uri = Uri.parse(`meta:${node.connLabel} - ${node.label}.json`); + metaContentProvider.update(uri, formattedDoc); + const document = await workspace.openTextDocument(uri); + await window.showTextDocument(document, { + preview: false, + viewColumn: ViewColumn.One, + }); + } else { + kdbOutputLog("[META] Meta content not found", "ERROR"); + } +} + export function writeQueryResultsToConsole( result: string | string[], query: string, @@ -654,7 +701,7 @@ export function writeScratchpadResult( duration, ); } else { - if (ext.resultsViewProvider.isVisible()) { + if (ext.isResultsTabVisible) { writeQueryResultsToView( result.data, query, diff --git a/src/commands/workspaceCommand.ts b/src/commands/workspaceCommand.ts index cda26751..c182ba77 100644 --- a/src/commands/workspaceCommand.ts +++ b/src/commands/workspaceCommand.ts @@ -32,7 +32,7 @@ import { InsightsNode, KdbNode } from "../services/kdbTreeProvider"; import { runQuery } from "./serverCommand"; import { ExecutionTypes } from "../models/execution"; import { importOldDsFiles, oldFilesExists } from "../utils/dataSource"; -import { offerConnectAction } from "../utils/core"; +import { kdbOutputLog, offerConnectAction } from "../utils/core"; import Path from "path"; const connectionService = new ConnectionManagementService(); @@ -357,8 +357,9 @@ export async function importOldDSFiles() { }, async (progress, token) => { token.onCancellationRequested(() => { - ext.outputChannel.appendLine( - "User cancelled the old DS files import.", + kdbOutputLog( + "[DATASOURCE] User cancelled the old DS files import.", + "INFO", ); return false; }); @@ -372,5 +373,9 @@ export async function importOldDSFiles() { window.showInformationMessage( "No old Datasource files found on your VSCODE.", ); + kdbOutputLog( + "[DATASOURCE] No old Datasource files found on your VSCODE.", + "INFO", + ); } } diff --git a/src/extension.ts b/src/extension.ts index acd4501c..f375b6b2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -41,12 +41,15 @@ import { } from "./commands/installTools"; import { activeConnection, + addAuthConnection, addInsightsConnection, addKdbConnection, addNewConnection, connect, disconnect, enableTLS, + openMeta, + refreshGetMeta, removeConnection, rerunQuery, } from "./commands/serverCommand"; @@ -57,9 +60,11 @@ import { InsightDetails, Insights } from "./models/insights"; import { QueryResult } from "./models/queryResult"; import { Server, ServerDetails } from "./models/server"; import { + InsightsMetaNode, InsightsNode, KdbNode, KdbTreeProvider, + MetaObjectPayloadNode, } from "./services/kdbTreeProvider"; import { QueryHistoryProvider, @@ -72,6 +77,7 @@ import { getInsights, getServers, initializeLocalServers, + kdbOutputLog, } from "./utils/core"; import { runQFileTerminal } from "./utils/execution"; import AuthSettings from "./utils/secretStorage"; @@ -94,6 +100,7 @@ import { createDefaultDataSourceFile } from "./models/dataSource"; import { connectBuildTools, lintCommand } from "./commands/buildToolsCommand"; import { CompletionProvider } from "./services/completionProvider"; import { QuickFixProvider } from "./services/quickFixProvider"; +import { connectClientCommands } from "./commands/clientCommands"; let client: LanguageClient; @@ -151,7 +158,7 @@ export async function activate(context: ExtensionContext) { AuthSettings.init(context); ext.secretSettings = AuthSettings.instance; - ext.outputChannel.appendLine("kdb extension is now active!"); + kdbOutputLog("kdb extension is now active!", "INFO"); try { // check for installed q runtime @@ -203,6 +210,25 @@ export async function activate(context: ExtensionContext) { activeConnection(viewItem); }, ), + commands.registerCommand( + "kdb.addAuthentication", + async (viewItem: KdbNode) => { + const username = await window.showInputBox({ + prompt: "Username", + title: "Add Authentication", + }); + if (username) { + const password = await window.showInputBox({ + prompt: "Password", + title: "Add Authentication", + password: true, + }); + if (password) { + await addAuthConnection(viewItem.children[0], username, password); + } + } + }, + ), commands.registerCommand("kdb.enableTLS", async (viewItem: KdbNode) => { await enableTLS(viewItem.children[0]); }), @@ -220,6 +246,12 @@ export async function activate(context: ExtensionContext) { await disconnect(connLabel); }, ), + commands.registerCommand( + "kdb.open.meta", + async (viewItem: InsightsMetaNode | MetaObjectPayloadNode) => { + await openMeta(viewItem); + }, + ), commands.registerCommand("kdb.addConnection", async () => { await addNewConnection(); }), @@ -247,9 +279,16 @@ export async function activate(context: ExtensionContext) { await removeConnection(viewItem); }, ), - commands.registerCommand("kdb.refreshServerObjects", () => { + commands.registerCommand("kdb.refreshServerObjects", async () => { ext.serverProvider.reload(); + await refreshGetMeta(); }), + commands.registerCommand( + "kdb.insights.refreshMeta", + async (viewItem: InsightsNode) => { + await refreshGetMeta(viewItem.label); + }, + ), commands.registerCommand( "kdb.queryHistory.rerun", (viewItem: QueryHistoryTreeItem) => { @@ -276,6 +315,7 @@ export async function activate(context: ExtensionContext) { commands.registerCommand( "kdb.stopLocalProcess", async (viewItem: KdbNode) => { + await commands.executeCommand("kdb.disconnect", viewItem); await stopLocalProcess(viewItem); }, ), @@ -452,7 +492,7 @@ export async function activate(context: ExtensionContext) { }, }; const clientOptions: LanguageClientOptions = { - documentSelector: [{ scheme: "file", language: "q" }], + documentSelector: [{ language: "q" }], synchronize: { fileEvents: workspace.createFileSystemWatcher("**/*.{q,quke}"), }, @@ -467,6 +507,8 @@ export async function activate(context: ExtensionContext) { await client.start(); + connectClientCommands(context, client); + Telemetry.sendEvent("Extension.Activated"); const yamlExtension = extensions.getExtension("redhat.vscode-yaml"); if (yamlExtension) { diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index 5192b5d1..735c5ab2 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -47,6 +47,7 @@ export namespace ext { export let serverProvider: KdbTreeProvider; export let queryHistoryProvider: QueryHistoryProvider; export let resultsViewProvider: KdbResultsViewProvider; + export let isResultsTabVisible: boolean; export let scratchpadTreeProvider: WorkspaceTreeProvider; export let dataSourceTreeProvider: WorkspaceTreeProvider; export let runScratchpadItem: StatusBarItem; @@ -133,6 +134,7 @@ export namespace ext { revoke: "auth/realms/insights/protocol/openid-connect/revoke", tokenURL: "auth/realms/insights/protocol/openid-connect/token", scratchpadURL: "servicebroker/scratchpad/display", + configURL: "kxicontroller/config", }; export const insightsScratchpadUrls = { diff --git a/src/ipc/QGuid.ts b/src/ipc/QGuid.ts index 7e351a0f..68fa792c 100644 --- a/src/ipc/QGuid.ts +++ b/src/ipc/QGuid.ts @@ -11,72 +11,49 @@ * specific language governing permissions and limitations under the License. */ -import { TypeBase, TypeNum } from "./typeBase"; +import { TypeNum } from "./typeBase"; import Vector from "./vector"; -export default class QFloat extends Vector { - static readonly typeNum = TypeNum.float; - +export default class QGuid extends Vector { constructor(length: number, offset: number, dataView: DataView) { - super(length, offset, TypeNum.float, dataView, 4); + super(length, offset, TypeNum.guid, dataView, 16); } - static listToIPC(values: Array): Uint8Array { - const size = values.length * 4 + 6; - const buffer = TypeBase.createBuffer(size); - - buffer.wb(QFloat.typeNum); - buffer.wb(0); - buffer.wi(values.length); - values.forEach((v) => QFloat.writeValue(v, buffer.wb)); - - return buffer.data; + calcRange(): bigint[] { + throw new Error("Not implemented"); } - static toIPC(value: number): Uint8Array { - const buffer = TypeBase.createBuffer(5); - buffer.wb(256 - QFloat.typeNum); - QFloat.writeValue(value, buffer.wb); - return buffer.data; + hash(i: number): number { + // return this.dataView.getFloat64(this.offset + i); + throw new Error("GUID hash not implemented " + i); } - static writeValue(value: number, wb: (b: number) => void): void { - if (value !== null) { - const fb = new Float32Array(1); - fb[0] = value; - new Uint8Array(fb.buffer).forEach((f) => wb(f)); - } else { - [0, 0, 192, 255].forEach((l) => wb(l)); - } + getScalar(i: number): bigint { + // TODO: should be 128-bit not 64 + return this.dataView.getBigInt64(this.getByteLocation(i), true); } - calcRange(): number[] { - let xMax = Number.NEGATIVE_INFINITY; - let xMin = Number.POSITIVE_INFINITY; - - for (let i = 0; i < this.length; i += 1) { - const x = this.getScalar(i); - if (isNaN(xMax) || (x > xMax && !isNaN(x))) { - xMax = x; - } - if (isNaN(xMin) || (x < xMin && !isNaN(x))) { - xMin = x; - } - } - - return [xMin, xMax]; - } + getValue(index: number): string | null { + const UUID_NULL = "00000000-0000-0000-0000-000000000000"; + const start = this.getByteLocation(index); + const end = start + this.size; + const x = "0123456789abcdef"; + let s = ""; + for (let i = start; i < end; i++) { + const c = this.dataView.getUint8(i); + const d = i - start; + s += d === 4 || d === 6 || d === 8 || d === 10 ? "-" : ""; - getScalar(i: number): number { - return this.dataView.getFloat32(this.getByteLocation(i), true); - } + // eslint-disable-next-line no-bitwise + s += x[c >> 4]; - getValue(i: number): number | null { - const val = this.getScalar(i); - return isNaN(val) ? null : val; - } + // eslint-disable-next-line no-bitwise + s += x[c & 15]; + } + if (s === UUID_NULL) { + return null; + } - hash(i: number): number { - return this.dataView.getFloat32(this.getHashLocation(i), true); + return s; } } diff --git a/src/models/config.ts b/src/models/config.ts new file mode 100644 index 00000000..76d05ee0 --- /dev/null +++ b/src/models/config.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) 1998-2023 Kx Systems Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +export type InsightsConfig = { + description?: string; + encryptionInFlight?: boolean; + installSize?: any; + restricted?: boolean; + storage?: any; + version: string; +}; + +export type InsightsEndpoints = { + scratchpad: { + scratchpad: string; + import: string; + importSql: string; + importQsql: string; + reset: string; + }; + serviceGateway: { + meta: string; + data: string; + sql: string; + qsql: string; + }; +}; diff --git a/src/models/insights.ts b/src/models/insights.ts index 4b13b0e1..bccf6667 100644 --- a/src/models/insights.ts +++ b/src/models/insights.ts @@ -15,6 +15,7 @@ export interface InsightDetails { alias: string; server: string; auth: boolean; + realm?: string; } export interface Insights { diff --git a/src/models/meta.ts b/src/models/meta.ts index 5e21f038..52f6e8b0 100644 --- a/src/models/meta.ts +++ b/src/models/meta.ts @@ -11,6 +11,15 @@ * specific language governing permissions and limitations under the License. */ +export enum MetaInfoType { + META = "meta", + SCHEMA = "schema", + API = "api", + AGG = "agg", + DAP = "dap", + RC = "rc", +} + export type MetaRC = { rc: string; labels: { diff --git a/src/services/connectionManagerService.ts b/src/services/connectionManagerService.ts index c9182884..d7c166a3 100644 --- a/src/services/connectionManagerService.ts +++ b/src/services/connectionManagerService.ts @@ -19,10 +19,11 @@ import { Telemetry } from "../utils/telemetryClient"; import { InsightsConnection } from "../classes/insightsConnection"; import { sanitizeQuery } from "../utils/queryUtils"; import { - getHash, getInsights, + getKeyForServerName, getServerName, getServers, + kdbOutputLog, removeLocalConnectionContext, updateInsights, updateServers, @@ -30,6 +31,7 @@ import { import { Insights } from "../models/insights"; import { Server } from "../models/server"; import { refreshDataSourcesPanel } from "../utils/dataSource"; +import { MetaInfoType } from "../models/meta"; export class ConnectionManagementService { public retrieveConnection( @@ -92,8 +94,9 @@ export class ConnectionManagementService { return; } if (conn) { - ext.outputChannel.appendLine( + kdbOutputLog( `Connection established successfully to: ${connLabel}`, + "CONNECTION", ); Telemetry.sendEvent("Connection.Connected.QProcess"); @@ -112,6 +115,14 @@ export class ConnectionManagementService { await insightsConn.connect(); if (insightsConn.connected) { Telemetry.sendEvent("Connection.Connected.Insights"); + kdbOutputLog( + `Connection established successfully to: ${connLabel}`, + "CONNECTION", + ); + kdbOutputLog( + `${connLabel} connection insights version: ${insightsConn.insightsVersion}`, + "CONNECTION", + ); ext.connectedConnectionList.push(insightsConn); this.isConnectedBehaviour(connection); } else { @@ -161,7 +172,7 @@ export class ConnectionManagementService { } if (connNode instanceof InsightsNode) { const insights = getInsights(); - const key = getHash(connNode.details.server); + const key = getKeyForServerName(connNode.details.alias); if (insights && insights[key]) { const uInsights = Object.keys(insights).filter((insight) => { return insight !== key; @@ -178,14 +189,7 @@ export class ConnectionManagementService { } else { const servers: Server | undefined = getServers(); - const key = - connNode.details.serverAlias != "" - ? getHash( - `${connNode.details.serverName}${connNode.details.serverPort}${connNode.details.serverAlias}`, - ) - : getHash( - `${connNode.details.serverName}${connNode.details.serverPort}`, - ); + const key = getKeyForServerName(connNode.details.serverAlias || ""); if (servers != undefined && servers[key]) { const uServers = Object.keys(servers).filter((server) => { return server !== key; @@ -247,10 +251,9 @@ export class ConnectionManagementService { } } Telemetry.sendEvent("Connection.Disconnected." + connType); - ext.outputChannel.appendLine( - `[${new Date().toLocaleTimeString()}] Connection disconnected: ${ - connection.connLabel - }`, + kdbOutputLog( + `[CONNECTION] Connection closed: ${connection.connLabel}`, + "INFO", ); ext.serverProvider.reload(); } @@ -339,4 +342,39 @@ export class ConnectionManagementService { await connection.getMeta(); } } + + public getMetaInfoType(value: string): MetaInfoType | undefined { + return MetaInfoType[value as keyof typeof MetaInfoType]; + } + + public retrieveMetaContent( + connLabel: string, + metaTypeString: string, + ): string { + const metaType = this.getMetaInfoType(metaTypeString.toUpperCase()); + if (!metaType) { + kdbOutputLog( + "[META] The meta info type that you try to open is not valid", + "ERROR", + ); + return ""; + } + const connection = this.retrieveConnectedConnection(connLabel); + if (!connection) { + kdbOutputLog( + "[META] The connection that you try to open meta info is not connected", + "ERROR", + ); + return ""; + } + if (connection instanceof LocalConnection) { + kdbOutputLog( + "[META] The connection that you try to open meta info is not an Insights connection", + "ERROR", + ); + return ""; + } + + return connection.returnMetaObject(metaType); + } } diff --git a/src/services/dataSourceEditorProvider.ts b/src/services/dataSourceEditorProvider.ts index 7ad1b758..2fed725c 100644 --- a/src/services/dataSourceEditorProvider.ts +++ b/src/services/dataSourceEditorProvider.ts @@ -41,7 +41,7 @@ import { import { InsightsConnection } from "../classes/insightsConnection"; import { MetaObjectPayload } from "../models/meta"; import { ConnectionManagementService } from "./connectionManagerService"; -import { offerConnectAction } from "../utils/core"; +import { kdbOutputLog, offerConnectAction } from "../utils/core"; export class DataSourceEditorProvider implements CustomTextEditorProvider { public filenname = ""; @@ -92,7 +92,7 @@ export class DataSourceEditorProvider implements CustomTextEditorProvider { ); meta = Promise.resolve({}); this.cache.set(connLabel, meta); - //TODO ADD ERROR TO CONSOLE HERE + kdbOutputLog("No database running in this Insights connection.", "ERROR"); } return (await meta) || Promise.resolve({}); } diff --git a/src/services/kdbInsights/codeFlowLogin.ts b/src/services/kdbInsights/codeFlowLogin.ts index d0bb688e..33c344b1 100644 --- a/src/services/kdbInsights/codeFlowLogin.ts +++ b/src/services/kdbInsights/codeFlowLogin.ts @@ -28,6 +28,27 @@ interface IDeferred { reject: (reason: any) => void; } +function getAuthUrl(insightsUrl: string, realm: string) { + return new url.URL( + `auth/realms/${realm}/protocol/openid-connect/auth`, + insightsUrl, + ); +} + +function getTokenUrl(insightsUrl: string, realm: string) { + return new url.URL( + `auth/realms/${realm}/protocol/openid-connect/token`, + insightsUrl, + ); +} + +function getRevokeUrl(insightsUrl: string, realm: string) { + return new url.URL( + `auth/realms/${realm}/protocol/openid-connect/revoke`, + insightsUrl, + ); +} + export interface IToken { accessToken: string; accessTokenExpirationDate: Date; @@ -41,7 +62,7 @@ const commonRequestParams = { client_id: "insights-app", }; -export async function signIn(insightsUrl: string) { +export async function signIn(insightsUrl: string, realm: string) { const { server, codePromise } = createServer(); try { @@ -54,17 +75,14 @@ export async function signIn(insightsUrl: string) { state: crypto.randomBytes(20).toString("hex"), }; - const authorizationUrl = new url.URL( - ext.insightsAuthUrls.authURL, - insightsUrl, - ); + const authorizationUrl = getAuthUrl(insightsUrl, realm); authorizationUrl.search = queryString(authParams); await env.openExternal(Uri.parse(authorizationUrl.toString())); const code = await codePromise; - return await getToken(insightsUrl, code); + return await getToken(insightsUrl, realm, code); } finally { setImmediate(() => server.close()); } @@ -72,6 +90,7 @@ export async function signIn(insightsUrl: string) { export async function signOut( insightsUrl: string, + realm: string, token: string, ): Promise { const queryParams = queryString({ @@ -84,7 +103,7 @@ export async function signOut( const headers = { headers: { "Content-Type": "application/x-www-form-urlencoded" }, }; - const requestUrl = new url.URL(ext.insightsAuthUrls.revoke, insightsUrl); + const requestUrl = getRevokeUrl(insightsUrl, realm); await axios.post(requestUrl.toString(), body, headers).then((res) => { return res.data; @@ -93,17 +112,20 @@ export async function signOut( export async function refreshToken( insightsUrl: string, + realm: string, token: string, ): Promise { - return await tokenRequest(insightsUrl, { + return await tokenRequest(insightsUrl, realm, { grant_type: ext.insightsGrantType.refreshToken, refresh_token: token, }); } +/* istanbul ignore next */ export async function getCurrentToken( serverName: string, serverAlias: string, + realm: string, ): Promise { if (serverName === "" || serverAlias === "") { return undefined; @@ -115,9 +137,9 @@ export async function getCurrentToken( if (existingToken !== undefined) { const storedToken: IToken = JSON.parse(existingToken); if (new Date(storedToken.accessTokenExpirationDate) < new Date()) { - token = await refreshToken(serverName, storedToken.refreshToken); + token = await refreshToken(serverName, realm, storedToken.refreshToken); if (token === undefined) { - token = await signIn(serverName); + token = await signIn(serverName, realm); ext.context.secrets.store(serverAlias, JSON.stringify(token)); } ext.context.secrets.store(serverAlias, JSON.stringify(token)); @@ -126,17 +148,19 @@ export async function getCurrentToken( return storedToken; } } else { - token = await signIn(serverName); + token = await signIn(serverName, realm); ext.context.secrets.store(serverAlias, JSON.stringify(token)); } return token; } +/* istanbul ignore next */ async function getToken( insightsUrl: string, + realm: string, code: string, ): Promise { - return await tokenRequest(insightsUrl, { + return await tokenRequest(insightsUrl, realm, { code, grant_type: ext.insightsGrantType.authorizationCode, }); @@ -144,6 +168,7 @@ async function getToken( async function tokenRequest( insightsUrl: string, + realm: string, params: any, ): Promise { const queryParams = queryString(params); @@ -155,7 +180,7 @@ async function tokenRequest( signal: AbortSignal.timeout(closeTimeout), }; - const requestUrl = new url.URL(ext.insightsAuthUrls.tokenURL, insightsUrl); + const requestUrl = getTokenUrl(insightsUrl, realm); let response; if (params.grant_type === "refresh_token") { diff --git a/src/services/kdbTreeProvider.ts b/src/services/kdbTreeProvider.ts index c2e15097..cb74b1c4 100644 --- a/src/services/kdbTreeProvider.ts +++ b/src/services/kdbTreeProvider.ts @@ -39,6 +39,8 @@ import { getServerName, getStatus, } from "../utils/core"; +import { ConnectionManagementService } from "./connectionManagerService"; +import { InsightsConnection } from "../classes/insightsConnection"; export class KdbTreeProvider implements TreeDataProvider { private _onDidChangeTreeData: EventEmitter< @@ -50,7 +52,7 @@ export class KdbTreeProvider implements TreeDataProvider { constructor( private serverList: Server, - private insightList: Insights, + private insightsList: Insights, ) {} reload(): void { @@ -64,7 +66,7 @@ export class KdbTreeProvider implements TreeDataProvider { } refreshInsights(insightsList: Insights): void { - this.insightList = insightsList; + this.insightsList = insightsList; this._onDidChangeTreeData.fire(); } @@ -76,6 +78,16 @@ export class KdbTreeProvider implements TreeDataProvider { ) { ext.isBundleQCreated = true; } + if ( + element instanceof InsightsMetaNode || + element instanceof MetaObjectPayloadNode + ) { + element.command = { + command: "kdb.open.meta", + title: "Open Meta Object", + arguments: [element], + }; + } return element; } @@ -83,7 +95,7 @@ export class KdbTreeProvider implements TreeDataProvider { if (!this.serverList) { return Promise.resolve([]); } - if (!this.insightList) { + if (!this.insightsList) { return Promise.resolve([]); } @@ -94,6 +106,11 @@ export class KdbTreeProvider implements TreeDataProvider { ext.kdbrootNodes.indexOf(element.contextValue) !== -1 ) { return Promise.resolve(await this.getNamespaces()); + } else if ( + element.contextValue !== undefined && + ext.kdbinsightsNodes.indexOf(element.contextValue) !== -1 + ) { + return Promise.resolve(await this.getMetas(element.contextValue)); } else if (element.contextValue === "ns") { return Promise.resolve( this.getCategories( @@ -101,6 +118,11 @@ export class KdbTreeProvider implements TreeDataProvider { ext.qObjectCategories, ), ); + } else if ( + element.contextValue === "meta" && + element instanceof InsightsMetaNode + ) { + return Promise.resolve(this.getMetaObjects(element.connLabel)); } else { return Promise.resolve(this.getServerObjects(element)); } @@ -124,7 +146,26 @@ export class KdbTreeProvider implements TreeDataProvider { // eslint-disable-next-line @typescript-eslint/no-unused-vars private getInsightsChildElements(_element?: InsightsNode): InsightsNode[] { - return this.createInsightLeafItems(this.insightList); + return this.createInsightLeafItems(this.insightsList); + } + + /* istanbul ignore next */ + private async getMetas(connLabel: string): Promise { + const connMng = new ConnectionManagementService(); + const conn = connMng.retrieveConnectedConnection(connLabel); + if (conn) { + return [ + new InsightsMetaNode( + [], + "meta", + "", + TreeItemCollapsibleState.Collapsed, + connLabel, + ), + ]; + } else { + return new Array(); + } } private async getNamespaces(): Promise { @@ -289,6 +330,86 @@ export class KdbTreeProvider implements TreeDataProvider { return new Array(); } + /* istanbul ignore next */ + private async getMetaObjects( + connLabel: string, + ): Promise { + const connMng = new ConnectionManagementService(); + const conn = connMng.retrieveConnectedConnection(connLabel); + const isInsights = conn instanceof InsightsConnection; + if (conn && isInsights) { + const meta = conn.meta; + if (!meta) { + return new Array(); + } + const objects: MetaObjectPayloadNode[] = []; + if (meta.payload.schema) { + objects.push( + new MetaObjectPayloadNode( + [], + "schema", + "", + TreeItemCollapsibleState.None, + "schemaicon", + connLabel, + ), + ); + } + if (meta.payload.api) { + objects.push( + new MetaObjectPayloadNode( + [], + "api", + "", + TreeItemCollapsibleState.None, + "apiicon", + connLabel, + ), + ); + } + if (meta.payload.dap) { + objects.push( + new MetaObjectPayloadNode( + [], + "dap", + "", + TreeItemCollapsibleState.None, + "dapicon", + connLabel, + ), + ); + } + if (meta.payload.rc) { + objects.push( + new MetaObjectPayloadNode( + [], + "rc", + "", + TreeItemCollapsibleState.None, + "rcicon", + connLabel, + ), + ); + } + if (meta.payload.agg) { + objects.push( + new MetaObjectPayloadNode( + [], + "agg", + "", + TreeItemCollapsibleState.None, + "aggicon", + connLabel, + ), + ); + } + + return objects; + } else { + return new Array(); + } + } + private createLeafItems(servers: Server): KdbNode[] { const keys: string[] = Object.keys(servers); return keys.map( @@ -305,16 +426,21 @@ export class KdbTreeProvider implements TreeDataProvider { } private createInsightLeafItems(insights: Insights): InsightsNode[] { + const connMng = new ConnectionManagementService(); const keys: string[] = Object.keys(insights); - return keys.map( - (x) => - new InsightsNode( - [], - insights[x].alias, - insights[x], - TreeItemCollapsibleState.None, - ), - ); + return keys.map((x) => { + const isConnected = connMng.retrieveConnectedConnection( + insights[x].alias, + ); + return new InsightsNode( + [], + insights[x].alias, + insights[x], + isConnected + ? TreeItemCollapsibleState.Collapsed + : TreeItemCollapsibleState.None, + ); + }); } } @@ -329,10 +455,6 @@ export class KdbNode extends TreeItem { label = label + ` [${details.serverAlias}]`; } - // if (ext.connectionNode != undefined && label === ext.connectionNode.label) { - // label = label + " (connected)"; - // } - // set context for root nodes if (ext.kdbrootNodes.indexOf(label) === -1) { ext.kdbrootNodes.push(label); @@ -488,6 +610,43 @@ export class InsightsNode extends TreeItem { contextValue = this.label; // "root"; } +export class InsightsMetaNode extends TreeItem { + constructor( + public readonly children: string[], + public readonly label: string, + public readonly details: string, + public readonly collapsibleState: TreeItemCollapsibleState, + public readonly connLabel: string, + ) { + super(label, collapsibleState); + this.description = this.getDescription(); + } + + getDescription(): string { + return ""; + } + + iconPath = { + light: path.join( + __filename, + "..", + "..", + "resources", + "metaIcons", + "metaicon.svg", + ), + dark: path.join( + __filename, + "..", + "..", + "resources", + "metaIcons", + "metaicon.svg", + ), + }; + contextValue = "meta"; +} + export class QNamespaceNode extends TreeItem { constructor( public readonly children: string[], @@ -557,6 +716,38 @@ export class QCategoryNode extends TreeItem { contextValue = this.ns; // "category"; } +export class MetaObjectPayloadNode extends TreeItem { + constructor( + public readonly children: string[], + public readonly label: string, + public readonly details: string, + public readonly collapsibleState: TreeItemCollapsibleState, + public readonly coreIcon: string, + public readonly connLabel: string, + ) { + super(label, collapsibleState); + this.description = ""; + } + iconPath = { + light: path.join( + __filename, + "..", + "..", + "resources", + "metaIcons", + `${this.coreIcon}.svg`, + ), + dark: path.join( + __filename, + "..", + "..", + "resources", + "metaIcons", + `${this.coreIcon}.svg`, + ), + }; +} + export class QServerNode extends TreeItem { constructor( public readonly children: string[], diff --git a/src/services/metaContentProvider.ts b/src/services/metaContentProvider.ts new file mode 100644 index 00000000..299f1bd7 --- /dev/null +++ b/src/services/metaContentProvider.ts @@ -0,0 +1,30 @@ +/* + * Copyright (c) 1998-2023 Kx Systems Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +import * as vscode from "vscode"; + +export class MetaContentProvider implements vscode.TextDocumentContentProvider { + private _onDidChange = new vscode.EventEmitter(); + public readonly onDidChange = this._onDidChange.event; + + private content: string = ""; + + public update(uri: vscode.Uri, content: string) { + this.content = content; + this._onDidChange.fire(uri); + } + + provideTextDocumentContent(uri: vscode.Uri): string { + return this.content; + } +} diff --git a/src/services/resultsPanelProvider.ts b/src/services/resultsPanelProvider.ts index 8c1d7f3a..ab7d1058 100644 --- a/src/services/resultsPanelProvider.ts +++ b/src/services/resultsPanelProvider.ts @@ -36,9 +36,10 @@ export class KdbResultsViewProvider implements WebviewViewProvider { this._colorTheme = window.activeColorTheme; this.updateResults(this._results); }); - // this.resolveWebviewView(webviewView); + ext.isResultsTabVisible = true; } + /* istanbul ignore next */ public resolveWebviewView(webviewView: WebviewView) { this._view = webviewView; @@ -52,6 +53,13 @@ export class KdbResultsViewProvider implements WebviewViewProvider { webviewView.webview.onDidReceiveMessage((data) => { webviewView.webview.html = this._getWebviewContent(data); }); + webviewView.onDidChangeVisibility(() => { + ext.isResultsTabVisible = webviewView.visible; + }); + + webviewView.onDidDispose(() => { + ext.isResultsTabVisible = false; + }); } public updateResults( diff --git a/src/utils/core.ts b/src/utils/core.ts index 66ff7264..6a1e4091 100644 --- a/src/utils/core.ts +++ b/src/utils/core.ts @@ -30,7 +30,7 @@ import { showRegistrationNotification } from "./registration"; import { Telemetry } from "./telemetryClient"; export function log(childProcess: ChildProcess): void { - ext.outputChannel.appendLine(`Process ${childProcess.pid!} killed`); + kdbOutputLog(`Process ${childProcess.pid} started`, "INFO"); } export async function checkOpenSslInstalled(): Promise { @@ -45,14 +45,15 @@ export async function checkOpenSslInstalled(): Promise { const matcher = /(\d+.\d+.\d+)/; const installedVersion = result.cmdOutput.match(matcher); - ext.outputChannel.appendLine( + kdbOutputLog( `Detected version ${installedVersion} of OpenSSL installed.`, + "INFO", ); return semver.clean(installedVersion ? installedVersion[1] : ""); } } catch (err) { - ext.outputChannel.appendLine(`Error in checking OpenSSL version: ${err}`); + kdbOutputLog(`Error in checking OpenSSL version: ${err}`, "ERROR"); Telemetry.sendException(err as Error); } return null; @@ -62,6 +63,26 @@ export function getHash(input: string): string { return createHash("sha256").update(input).digest("base64"); } +export function getKeyForServerName(name: string) { + const conf = workspace.getConfiguration("kdb"); + const servers = conf.get<{ [key: string]: { serverAlias: string } }>( + "servers", + {}, + ); + let result = Object.keys(servers).find( + (key) => servers[key].serverAlias === name, + ); + if (result) { + return result; + } + const insgihts = conf.get<{ [key: string]: { alias: string } }>( + "insightsEnterpriseConnections", + {}, + ); + result = Object.keys(insgihts).find((key) => insgihts[key].alias === name); + return result || ""; +} + export function initializeLocalServers(servers: Server): void { Object.keys(servers!).forEach((server) => { if (servers![server].managed === true) { @@ -125,9 +146,7 @@ export function saveLocalProcessObj( args: string[], ): void { window.showInformationMessage("q process started successfully!"); - ext.outputChannel.appendLine( - `Child process id ${childProcess.pid!} saved in cache.`, - ); + kdbOutputLog(`Child process id ${childProcess.pid} saved in cache.`, "INFO"); ext.localProcessObjects[args[2]] = childProcess; } @@ -250,6 +269,32 @@ export function getServerAlias(serverList: ServerDetails[]): void { }); } +export function kdbOutputLog(message: string, type: string): void { + const dateNow = new Date().toLocaleDateString(); + const timeNow = new Date().toLocaleTimeString(); + ext.outputChannel.appendLine(`[${dateNow} ${timeNow}] [${type}] ${message}`); +} + +export function tokenUndefinedError(connLabel: string): void { + kdbOutputLog( + `Error retrieving access token for Insights connection named: ${connLabel}`, + "ERROR", + ); + window.showErrorMessage( + `Error retrieving access token for Insights connection named: ${connLabel}`, + ); +} + +export function invalidUsernameJWT(connLabel: string): void { + kdbOutputLog( + `JWT did not contain a valid preferred username for Insights connection: ${connLabel}`, + "ERROR", + ); + window.showErrorMessage( + `JWT did not contain a valid preferred username for Insights connection: ${connLabel}`, + ); +} + /* istanbul ignore next */ export function offerConnectAction(connLabel: string): void { window @@ -311,7 +356,7 @@ export async function checkLocalInstall(): Promise { if (QHOME || env.QHOME) { env.QHOME = QHOME || env.QHOME; if (!pathExists(env.QHOME!)) { - ext.outputChannel.appendLine("QHOME path stored is empty"); + kdbOutputLog("QHOME path stored is empty", "ERROR"); } await writeFile( join(__dirname, "qinstall.md"), @@ -323,7 +368,7 @@ export async function checkLocalInstall(): Promise { .getConfiguration() .update("kdb.qHomeDirectory", env.QHOME, ConfigurationTarget.Global); - ext.outputChannel.appendLine(`Installation of q found here: ${env.QHOME}`); + kdbOutputLog(`Installation of q found here: ${env.QHOME}`, "INFO"); showRegistrationNotification(); diff --git a/src/utils/cpUtils.ts b/src/utils/cpUtils.ts index 907665f1..c1f5553d 100644 --- a/src/utils/cpUtils.ts +++ b/src/utils/cpUtils.ts @@ -15,6 +15,7 @@ import * as cp from "child_process"; import * as os from "os"; import { join } from "path"; import { ext } from "../extensionVariables"; +import { kdbOutputLog } from "./core"; export async function executeCommand( workingDirectory: string | undefined, @@ -26,16 +27,17 @@ export async function executeCommand( workingDirectory, command, spawnCallback, - ...args + ...args, ); + ext.outputChannel.show(); if (result.code !== 0) { - ext.outputChannel.show(); throw new Error( - `Failed to run ${command} command. Check output window for more details.` + `Failed to run ${command} command. Check output window for more details.`, ); } else { - ext.outputChannel.append( - `Finished running command: ${command} ${result.formattedArgs}` + kdbOutputLog( + `Finished running command: ${command} ${result.formattedArgs}`, + "INFO", ); } return result.cmdOutput; @@ -50,7 +52,7 @@ export async function tryExecuteCommand( return await new Promise( ( resolve: (res: ICommandResult) => void, - reject: (e: Error) => void + reject: (e: Error) => void, ): void => { let cmdOutput = ""; let cmdOutputIncludingStderr = ""; @@ -78,24 +80,24 @@ export async function tryExecuteCommand( data = data.toString(); cmdOutput = cmdOutput.concat(data); cmdOutputIncludingStderr = cmdOutputIncludingStderr.concat(data); - ext.outputChannel.append(data); + kdbOutputLog(data, "INFO"); }); childProc.stderr?.on("data", (data: string | Buffer) => { data = data.toString(); cmdOutputIncludingStderr = cmdOutputIncludingStderr.concat(data); - ext.outputChannel.append(data); + kdbOutputLog(data, "INFO"); }); childProc.on("error", (error) => { - console.log(error); + kdbOutputLog(error.message, "ERROR"); reject(error); }); childProc.on("close", (code: number) => { resolve({ code, cmdOutput, cmdOutputIncludingStderr, formattedArgs }); }); - } + }, ); } diff --git a/src/utils/dataSource.ts b/src/utils/dataSource.ts index f68d2dce..44484027 100644 --- a/src/utils/dataSource.ts +++ b/src/utils/dataSource.ts @@ -19,19 +19,22 @@ import { DataSourcesPanel } from "../panels/datasource"; import { InsightsConnection } from "../classes/insightsConnection"; import { workspace, window, Uri } from "vscode"; import { Telemetry } from "./telemetryClient"; +import { kdbOutputLog } from "./core"; export function createKdbDataSourcesFolder(): string { const rootPath = ext.context.globalStorageUri.fsPath; const kdbDataSourcesFolderPath = path.join(rootPath, ext.kdbDataSourceFolder); if (!fs.existsSync(rootPath)) { - ext.outputChannel.appendLine( - `Directory created to the extension folder: ${rootPath}`, + kdbOutputLog( + `[DATSOURCE] Directory created to the extension folder: ${rootPath}`, + "INFO", ); fs.mkdirSync(rootPath); } if (!fs.existsSync(kdbDataSourcesFolderPath)) { - ext.outputChannel.appendLine( - `Directory created to the extension folder: ${kdbDataSourcesFolderPath}`, + kdbOutputLog( + `[DATSOURCE] Directory created to the extension folder: ${kdbDataSourcesFolderPath}`, + "INFO", ); fs.mkdirSync(kdbDataSourcesFolderPath); } @@ -47,6 +50,10 @@ export function convertTimeToTimestamp(time: string): string { const timePart = parts[1].replace("Z", "0").padEnd(9, "0"); return `${datePart}.${timePart}`; } catch (error) { + kdbOutputLog( + `The string param is in an incorrect format. Param: ${time} Error: ${error}`, + "ERROR", + ); console.error( `The string param is in an incorrect format. Param: ${time} Error: ${error}`, ); diff --git a/src/utils/queryUtils.ts b/src/utils/queryUtils.ts index 3d3bd003..ac60a394 100644 --- a/src/utils/queryUtils.ts +++ b/src/utils/queryUtils.ts @@ -21,6 +21,7 @@ import { DDateClass, DDateTimeClass, DTimestampClass } from "../ipc/cClasses"; import { TypeBase } from "../ipc/typeBase"; import { DataSourceFiles, DataSourceTypes } from "../models/dataSource"; import { QueryHistory } from "../models/queryHistory"; +import { kdbOutputLog } from "./core"; export function sanitizeQuery(query: string): string { if (query[0] === "`") { @@ -90,7 +91,7 @@ export function handleWSError(ab: ArrayBuffer): any { } } - ext.outputChannel.appendLine(`Error : ${errorString}`); + kdbOutputLog(`Error : ${errorString}`, "ERROR"); return { error: errorString }; } @@ -109,7 +110,7 @@ export function handleWSResults(ab: ArrayBuffer): any { if (res.rows.length === 0 && res.columns.length === 0) { return "No results found."; } - if (ext.resultsViewProvider.isVisible()) { + if (ext.isResultsTabVisible) { return getValueFromArray(res); } return convertRows(res.rows); diff --git a/src/utils/shell.ts b/src/utils/shell.ts index 4b7bfc8a..4a0fb53b 100644 --- a/src/utils/shell.ts +++ b/src/utils/shell.ts @@ -12,13 +12,13 @@ */ import { ChildProcess } from "node:child_process"; -import { ext } from "../extensionVariables"; import { ICommandResult, tryExecuteCommand } from "./cpUtils"; +import { kdbOutputLog } from "./core"; const isWin = process.platform === "win32"; export function log(childProcess: ChildProcess): void { - ext.outputChannel.appendLine(`Process ${childProcess.pid!} killed`); + kdbOutputLog(`Process ${childProcess.pid} killed`, "INFO"); } export async function killPid(pid = NaN): Promise { @@ -32,7 +32,7 @@ export async function killPid(pid = NaN): Promise { } else if (process.platform === "darwin") { result = await tryExecuteCommand("/bin", killPidCommand(pid), log); } - ext.outputChannel.appendLine(`Destroying q process result: ${result}`); + kdbOutputLog(`Destroying q process result: ${result}`, "INFO"); } function killPidCommand(pid: number): string { diff --git a/src/validators/kdbValidator.ts b/src/validators/kdbValidator.ts index 9b3f803d..bdbe6655 100644 --- a/src/validators/kdbValidator.ts +++ b/src/validators/kdbValidator.ts @@ -104,5 +104,7 @@ export function validateTls(input: string | undefined): string | undefined { } export function isAliasInUse(alias: string): boolean { - return ext.kdbConnectionAliasList.includes(alias); + return !!ext.kdbConnectionAliasList.find( + (item) => item.toLowerCase() === alias.toLowerCase(), + ); } diff --git a/src/webview/components/kdbNewConnectionView.ts b/src/webview/components/kdbNewConnectionView.ts index a106e398..63455338 100644 --- a/src/webview/components/kdbNewConnectionView.ts +++ b/src/webview/components/kdbNewConnectionView.ts @@ -52,6 +52,7 @@ export class KdbNewConnectionView extends LitElement { alias: "", server: "", auth: true, + realm: "", }; this.bundledServer = { serverName: "127.0.0.1", @@ -220,6 +221,29 @@ export class KdbNewConnectionView extends LitElement { `; } + renderRealm() { + return html` +
+ Define Realm (optional) +
+
+ Specify the Keycloak realm for authentication. Use this field to connect + to a specific realm as configured on your server. +
+ `; + } + tabClickAction(tabNumber: number) { const config = this.tabConfig[tabNumber as keyof typeof this.tabConfig] || @@ -381,6 +405,14 @@ export class KdbNewConnectionView extends LitElement { ${this.renderConnAddress(ServerType.INSIGHTS)} +
+
+
+ Advanced + ${this.renderRealm()} +
+
+
diff --git a/test/runTest.ts b/test/runTest.ts index 774e2184..0d8d9a71 100644 --- a/test/runTest.ts +++ b/test/runTest.ts @@ -27,7 +27,7 @@ async function main() { // load the instrumented files extensionTestsPath = path.join( __dirname, - "../../out-cov/test/suite/index", + "../../out-cov/test/suite/index" ); // signal that the coverage data should be gathered @@ -37,7 +37,6 @@ async function main() { await runTests({ extensionDevelopmentPath, extensionTestsPath, - version: "1.89.1", }); } catch (err) { console.log(err); diff --git a/test/suite/commands.test.ts b/test/suite/commands.test.ts index 92a692cd..fbd7ed44 100644 --- a/test/suite/commands.test.ts +++ b/test/suite/commands.test.ts @@ -15,7 +15,6 @@ import assert from "assert"; import mock from "mock-fs"; import * as sinon from "sinon"; import * as vscode from "vscode"; -import { TreeItemCollapsibleState, window } from "vscode"; import * as dataSourceCommand from "../../src/commands/dataSourceCommand"; import * as installTools from "../../src/commands/installTools"; import * as serverCommand from "../../src/commands/serverCommand"; @@ -34,6 +33,7 @@ import { InsightsNode, KdbNode, KdbTreeProvider, + MetaObjectPayloadNode, } from "../../src/services/kdbTreeProvider"; import { KdbResultsViewProvider } from "../../src/services/resultsPanelProvider"; import * as coreUtils from "../../src/utils/core"; @@ -50,6 +50,10 @@ import { InsightsConnection } from "../../src/classes/insightsConnection"; import * as workspaceCommand from "../../src/commands/workspaceCommand"; import { MetaObject } from "../../src/models/meta"; import { WorkspaceTreeProvider } from "../../src/services/workspaceTreeProvider"; +import { GetDataError } from "../../src/models/data"; +import * as clientCommand from "../../src/commands/clientCommands"; +import { LanguageClient } from "vscode-languageclient/node"; +import { MetaContentProvider } from "../../src/services/metaContentProvider"; describe("dataSourceCommand", () => { afterEach(() => { @@ -57,7 +61,7 @@ describe("dataSourceCommand", () => { mock.restore(); }); - it("should add a data source", async () => { + it.skip("should add a data source", async () => { mock({ "/temp": { ".kdb-datasources": { @@ -88,7 +92,7 @@ describe("dataSourceCommand2", () => { alias: "insightsserveralias", auth: true, }, - TreeItemCollapsibleState.None, + vscode.TreeItemCollapsibleState.None, ); const insightsConn = new InsightsConnection(insightsNode.label, insightsNode); const uriTest: vscode.Uri = vscode.Uri.parse("test"); @@ -629,6 +633,7 @@ describe("dataSourceCommand2", () => { afterEach(() => { sinon.restore(); + ext.isResultsTabVisible = false; }); it("should show an error message if not connected to an Insights server", async () => { @@ -665,7 +670,7 @@ describe("dataSourceCommand2", () => { insightsConn.meta = dummyMeta; getMetaStub.resolves(dummyMeta); getDataInsightsStub.resolves({ arrayBuffer: ab, error: "" }); - isVisibleStub.returns(true); + ext.isResultsTabVisible = true; await dataSourceCommand.runDataSource( dummyFileContent as DataSourceFiles, insightsConn.connLabel, @@ -684,7 +689,7 @@ describe("dataSourceCommand2", () => { dummyFileContent.dataSource.selectedType = DataSourceTypes.API; getMetaStub.resolves(dummyMeta); getDataInsightsStub.resolves({ arrayBuffer: ab, error: "" }); - isVisibleStub.returns(false); + ext.isResultsTabVisible = false; await dataSourceCommand.runDataSource( dummyFileContent as DataSourceFiles, insightsConn.connLabel, @@ -703,7 +708,7 @@ describe("dataSourceCommand2", () => { dummyFileContent.dataSource.selectedType = DataSourceTypes.SQL; getMetaStub.resolves(dummyMeta); getDataInsightsStub.resolves({ arrayBuffer: ab, error: "" }); - isVisibleStub.returns(false); + ext.isResultsTabVisible = false; await dataSourceCommand.runDataSource( dummyFileContent as DataSourceFiles, insightsConn.connLabel, @@ -798,6 +803,16 @@ describe("dataSourceCommand2", () => { sinon.assert.neverCalledWith(writeQueryResultsToViewStub); sinon.assert.neverCalledWith(writeQueryResultsToConsoleStub); }); + + it("should handle errors correctly", async () => { + retrieveConnStub.throws(new Error("Test error")); + await dataSourceCommand.runDataSource( + dummyFileContent as DataSourceFiles, + insightsConn.connLabel, + "test-file.kdb.json", + ); + windowMock.expects("showErrorMessage").once().withArgs("Test error"); + }); }); describe("populateScratchpad", async () => { @@ -848,6 +863,29 @@ describe("dataSourceCommand2", () => { .withArgs("Please connect to an Insights server"); }); }); + + describe("parseError", () => { + let kdbOutputLogStub: sinon.SinonStub; + + beforeEach(() => { + kdbOutputLogStub = sinon.stub(coreUtils, "kdbOutputLog"); + }); + afterEach(() => { + sinon.restore(); + }); + + it("should call kdbOutputLog and return error if error does not have buffer", () => { + const error: GetDataError = "test error"; + + const result = dataSourceCommand.parseError(error); + + assert(kdbOutputLogStub.calledOnce); + assert( + kdbOutputLogStub.calledWith(`[DATASOURCE] Error: ${error}`, "ERROR"), + ); + assert.deepEqual(result, { error }); + }); + }); }); describe("installTools", () => { @@ -874,14 +912,14 @@ describe("serverCommand", () => { alias: "insightsserveralias", auth: true, }, - TreeItemCollapsibleState.None, + vscode.TreeItemCollapsibleState.None, ); const kdbNode = new KdbNode( ["child1"], "testElement", servers["testServer"], - TreeItemCollapsibleState.None, + vscode.TreeItemCollapsibleState.None, ); const insights = { testInsight: { @@ -896,10 +934,10 @@ describe("serverCommand", () => { ext.serverProvider = undefined; }); - it("should call the New Connection Panel Renderer", () => { + it("should call the New Connection Panel Renderer", async () => { const newConnectionPanelStub = sinon.stub(NewConnectionPannel, "render"); - - serverCommand.addNewConnection(); + ext.context = {}; + await serverCommand.addNewConnection(); sinon.assert.calledOnce(newConnectionPanelStub); sinon.restore(); }); @@ -1669,6 +1707,7 @@ describe("serverCommand", () => { disconnectStub, getServersStub, getHashStub, + getKeyStub, getInsightsStub, removeLocalConnectionContextStub, updateServersStub, @@ -1680,6 +1719,7 @@ describe("serverCommand", () => { getServersStub = sinon.stub(coreUtils, "getServers"); getInsightsStub = sinon.stub(coreUtils, "getInsights"); getHashStub = sinon.stub(coreUtils, "getHash"); + getKeyStub = sinon.stub(coreUtils, "getKeyForServerName"); removeLocalConnectionContextStub = sinon.stub( coreUtils, "removeLocalConnectionContext", @@ -1698,7 +1738,7 @@ describe("serverCommand", () => { it("should remove connection and refresh server provider", async () => { indexOfStub.returns(1); getServersStub.returns({ testKey: {} }); - getHashStub.returns("testKey"); + getKeyStub.returns("testKey"); await serverCommand.removeConnection(kdbNode); @@ -1715,7 +1755,7 @@ describe("serverCommand", () => { ext.connectedContextStrings.push(kdbNode.label); indexOfStub.returns(1); getServersStub.returns({ testKey: {} }); - getHashStub.returns("testKey"); + getKeyStub.returns("testKey"); await serverCommand.removeConnection(kdbNode); assert.ok(updateServersStub.calledOnce); @@ -1752,6 +1792,90 @@ describe("serverCommand", () => { windowErrorStub.calledOnce; }); }); + + describe("refreshGetMeta", () => { + let refreshGetMetaStub, refreshAllGetMetasStub: sinon.SinonStub; + beforeEach(() => { + refreshGetMetaStub = sinon.stub( + ConnectionManagementService.prototype, + "refreshGetMeta", + ); + refreshAllGetMetasStub = sinon.stub( + ConnectionManagementService.prototype, + "refreshAllGetMetas", + ); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should call refreshGetMeta if connLabel is provided", async () => { + await serverCommand.refreshGetMeta("test"); + + sinon.assert.calledOnce(refreshGetMetaStub); + sinon.assert.calledWith(refreshGetMetaStub, "test"); + sinon.assert.notCalled(refreshAllGetMetasStub); + }); + + it("should call refreshAllGetMetas if connLabel is not provided", async () => { + await serverCommand.refreshGetMeta(); + + sinon.assert.notCalled(refreshGetMetaStub); + sinon.assert.calledOnce(refreshAllGetMetasStub); + }); + }); + + describe("openMeta", () => { + let sandbox: sinon.SinonSandbox; + const node = new MetaObjectPayloadNode( + [], + "meta", + "", + vscode.TreeItemCollapsibleState.None, + "meta", + "connLabel", + ); + const connService = new ConnectionManagementService(); + + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.spy(vscode.workspace, "registerTextDocumentContentProvider"); + sandbox.spy(vscode.workspace, "openTextDocument"); + sandbox.spy(vscode.window, "showTextDocument"); + }); + + afterEach(() => { + sandbox.restore(); + sinon.restore(); + }); + + it("should call functions once for valid meta content", async () => { + sinon + .stub(ConnectionManagementService.prototype, "retrieveMetaContent") + .returns('{"test": []}'); + await serverCommand.openMeta(node); + sinon.assert.calledOnce( + vscode.workspace.registerTextDocumentContentProvider as sinon.SinonSpy, + ); + sinon.assert.calledOnce( + vscode.workspace.openTextDocument as sinon.SinonSpy, + ); + sinon.assert.calledOnce(vscode.window.showTextDocument as sinon.SinonSpy); + }); + + it("should not call some functions for invalid meta content", async () => { + sinon.stub(connService, "retrieveMetaContent").returns(""); + await serverCommand.openMeta(node); + sinon.assert.calledOnce( + vscode.workspace.registerTextDocumentContentProvider as sinon.SinonSpy, + ); + sinon.assert.notCalled( + vscode.workspace.openTextDocument as sinon.SinonSpy, + ); + sinon.assert.notCalled(vscode.window.showTextDocument as sinon.SinonSpy); + }); + }); }); describe("walkthroughCommand", () => { @@ -1832,14 +1956,14 @@ describe("workspaceCommand", () => { }); describe("pickConnection", () => { it("should pick from available servers", async () => { - sinon.stub(window, "showQuickPick").value(async () => "test"); + sinon.stub(vscode.window, "showQuickPick").value(async () => "test"); const result = await workspaceCommand.pickConnection( vscode.Uri.file("test.kdb.q"), ); assert.strictEqual(result, "test"); }); it("should return undefined from (none)", async () => { - sinon.stub(window, "showQuickPick").value(async () => "(none)"); + sinon.stub(vscode.window, "showQuickPick").value(async () => "(none)"); const result = await workspaceCommand.pickConnection( vscode.Uri.file("test.kdb.q"), ); @@ -1878,17 +2002,26 @@ describe("workspaceCommand", () => { let windowErrorStub, windowWithProgressStub, windowShowInfo, - workspaceFolderStub: sinon.SinonStub; + workspaceFolderStub, + tokenOnCancellationRequestedStub, + kdbOutputLogStub: sinon.SinonStub; beforeEach(() => { windowErrorStub = sinon.stub(vscode.window, "showErrorMessage"); windowWithProgressStub = sinon.stub(vscode.window, "withProgress"); windowShowInfo = sinon.stub(vscode.window, "showInformationMessage"); workspaceFolderStub = sinon.stub(vscode.workspace, "workspaceFolders"); + tokenOnCancellationRequestedStub = sinon.stub(); + windowWithProgressStub.callsFake((options, task) => { + const token = { + onCancellationRequested: tokenOnCancellationRequestedStub, + }; + task({}, token); + }); + + kdbOutputLogStub = sinon.stub(coreUtils, "kdbOutputLog"); }); afterEach(() => { - windowErrorStub.restore(); - windowWithProgressStub.restore(); - windowShowInfo.restore(); + sinon.restore(); }); it("should show info message if old files do not exist", async () => { ext.oldDSformatExists = false; @@ -1913,5 +2046,90 @@ describe("workspaceCommand", () => { sinon.assert.notCalled(windowErrorStub); sinon.assert.notCalled(windowShowInfo); }); + + it("should log cancellation if user cancels the request", async () => { + ext.oldDSformatExists = true; + workspaceFolderStub.get(() => [ + { + uri: { fsPath: "path/to/workspace" }, + name: "workspace", + index: 0, + }, + ]); + + tokenOnCancellationRequestedStub.callsFake((callback) => callback()); + + await workspaceCommand.importOldDSFiles(); + + sinon.assert.calledOnce(kdbOutputLogStub); + sinon.assert.calledWith( + kdbOutputLogStub, + "[DATASOURCE] User cancelled the old DS files import.", + "INFO", + ); + }); + }); +}); + +describe("clientCommands", () => { + const client = sinon.createStubInstance(LanguageClient); + let executeBlock; + let toggleParameterCache; + + beforeEach(() => { + const context = { subscriptions: [] }; + sinon.stub(vscode.commands, "registerCommand").value((a, b) => b); + clientCommand.connectClientCommands(context, client); + executeBlock = context.subscriptions[0]; + toggleParameterCache = context.subscriptions[1]; + ext.activeTextEditor = { + options: { insertSpaces: true, indentSize: 4 }, + selection: { active: new vscode.Position(0, 0) }, + document: { + uri: vscode.Uri.file("/tmp/some.q"), + getText: () => "", + }, + }; + }); + afterEach(() => { + sinon.restore(); + ext.activeTextEditor = undefined; + }); + describe("executeBlock", () => { + it("should execute current block", async () => { + sinon + .stub(client, "sendRequest") + .value(async () => new vscode.Range(0, 0, 1, 1)); + sinon.stub(workspaceCommand, "runActiveEditor").value(() => {}); + await executeBlock(client); + assert.deepEqual( + ext.activeTextEditor.selection, + new vscode.Selection(0, 0, 1, 1), + ); + }); + }); + describe("kdb.toggleParameterCache", () => { + it("should add parameter cache for single line functions", async () => { + let edit: vscode.WorkspaceEdit; + sinon.stub(client, "sendRequest").value(async () => ({ + params: ["a"], + start: new vscode.Position(0, 0), + end: new vscode.Position(0, 10), + })); + sinon.stub(vscode.workspace, "applyEdit").value(async (a) => (edit = a)); + await toggleParameterCache(client); + assert.strictEqual(edit.size, 1); + }); + it("should add parameter cache for multi line functions", async () => { + let edit: vscode.WorkspaceEdit; + sinon.stub(client, "sendRequest").value(async () => ({ + params: ["a"], + start: new vscode.Position(0, 0), + end: new vscode.Position(1, 10), + })); + sinon.stub(vscode.workspace, "applyEdit").value(async (a) => (edit = a)); + await toggleParameterCache(client); + assert.strictEqual(edit.size, 1); + }); }); }); diff --git a/test/suite/qLangServer.test.ts b/test/suite/qLangServer.test.ts index 33105551..f8eaf616 100644 --- a/test/suite/qLangServer.test.ts +++ b/test/suite/qLangServer.test.ts @@ -28,10 +28,10 @@ const context = { includeDeclaration: true }; describe("qLangServer", () => { let server: QLangServer; - function createDocument(content: string) { + function createDocument(content: string, offset?: number) { content = content.trim(); const document = TextDocument.create("test.q", "q", 1, content); - const position = document.positionAt(content.length); + const position = document.positionAt(offset || content.length); const textDocument = TextDocumentIdentifier.create("test.q"); sinon.stub(server.documents, "get").value(() => document); return { @@ -55,6 +55,8 @@ describe("qLangServer", () => { onRenameRequest() {}, onCompletion() {}, onDidChangeConfiguration() {}, + onRequest() {}, + onSelectionRanges() {}, }); const params = { @@ -77,6 +79,7 @@ describe("qLangServer", () => { assert.ok(capabilities.definitionProvider); assert.ok(capabilities.renameProvider); assert.ok(capabilities.completionProvider); + assert.ok(capabilities.selectionRangeProvider); }); }); @@ -207,4 +210,124 @@ describe("qLangServer", () => { assert.strictEqual(result.length, 2); }); }); + + describe("onExpressionRange", () => { + it("should return the range of the expression", () => { + const params = createDocument("a:1;"); + const result = server.onExpressionRange(params); + assert.strictEqual(result.start.line, 0); + assert.strictEqual(result.start.character, 0); + assert.strictEqual(result.end.line, 0); + assert.strictEqual(result.end.character, 3); + }); + it("should return the range of the expression", () => { + const params = createDocument("a"); + const result = server.onExpressionRange(params); + assert.strictEqual(result.start.line, 0); + assert.strictEqual(result.start.character, 0); + assert.strictEqual(result.end.line, 0); + assert.strictEqual(result.end.character, 1); + }); + it("should return null", () => { + const params = createDocument(""); + const result = server.onExpressionRange(params); + assert.strictEqual(result, null); + }); + it("should return null", () => { + const params = createDocument(";"); + const result = server.onExpressionRange(params); + assert.strictEqual(result, null); + }); + it("should return null", () => { + const params = createDocument("/a:1"); + const result = server.onExpressionRange(params); + assert.strictEqual(result, null); + }); + }); + + describe("onExpressionRange", () => { + it("should return range for simple expression", () => { + const params = createDocument("a:1;"); + const result = server.onExpressionRange(params); + assert.ok(result); + assert.strictEqual(result.start.line, 0); + assert.strictEqual(result.start.character, 0); + assert.strictEqual(result.end.line, 0); + assert.strictEqual(result.end.character, 3); + }); + it("should return range for lambda", () => { + const params = createDocument("a:{b:1;b};"); + const result = server.onExpressionRange(params); + assert.ok(result); + assert.strictEqual(result.start.line, 0); + assert.strictEqual(result.start.character, 0); + assert.strictEqual(result.end.line, 0); + assert.strictEqual(result.end.character, 9); + }); + it("should skip comments", () => { + const params = createDocument("a:1 /comment", 1); + const result = server.onExpressionRange(params); + assert.ok(result); + assert.strictEqual(result.start.line, 0); + assert.strictEqual(result.start.character, 0); + assert.strictEqual(result.end.line, 0); + assert.strictEqual(result.end.character, 3); + }); + }); + + describe("omParameterCache", () => { + it("should cache paramater", () => { + const params = createDocument("{[a;b]}"); + const result = server.onParameterCache(params); + assert.ok(result); + assert.deepEqual(result.params, ["a", "b"]); + assert.strictEqual(result.start.line, 0); + assert.strictEqual(result.start.character, 6); + assert.strictEqual(result.end.line, 0); + assert.strictEqual(result.end.character, 6); + }); + it("should cache paramater", () => { + const params = createDocument("{[a;b]\n }"); + const result = server.onParameterCache(params); + assert.ok(result); + assert.deepEqual(result.params, ["a", "b"]); + assert.strictEqual(result.start.line, 0); + assert.strictEqual(result.start.character, 6); + assert.strictEqual(result.end.line, 1); + assert.strictEqual(result.end.character, 1); + }); + it("should return null", () => { + const params = createDocument("{[]}"); + const result = server.onParameterCache(params); + assert.strictEqual(result, null); + }); + it("should return null", () => { + const params = createDocument("{}"); + const result = server.onParameterCache(params); + assert.strictEqual(result, null); + }); + it("should return null", () => { + const params = createDocument("a:1;"); + const result = server.onParameterCache(params); + assert.strictEqual(result, null); + }); + it("should return null", () => { + const params = createDocument(""); + const result = server.onParameterCache(params); + assert.strictEqual(result, null); + }); + }); + + describe("onSelectionRanges", () => { + it("should return identifier range", () => { + const params = createDocument(".test.ref"); + const result = server.onSelectionRanges({ + textDocument: params.textDocument, + positions: [params.position], + }); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].range.start.character, 0); + assert.strictEqual(result[0].range.end.character, 9); + }); + }); }); diff --git a/test/suite/services.test.ts b/test/suite/services.test.ts index 39e3d649..71163bc5 100644 --- a/test/suite/services.test.ts +++ b/test/suite/services.test.ts @@ -35,9 +35,11 @@ import { signOut, } from "../../src/services/kdbInsights/codeFlowLogin"; import { + InsightsMetaNode, InsightsNode, KdbNode, KdbTreeProvider, + MetaObjectPayloadNode, QCategoryNode, QNamespaceNode, QServerNode, @@ -58,12 +60,70 @@ import { } from "../../src/services/workspaceTreeProvider"; import Path from "path"; import * as utils from "../../src/utils/getUri"; -import { MetaObject } from "../../src/models/meta"; +import { MetaInfoType, MetaObject } from "../../src/models/meta"; import { CompletionProvider } from "../../src/services/completionProvider"; +import { MetaContentProvider } from "../../src/services/metaContentProvider"; // eslint-disable-next-line @typescript-eslint/no-var-requires const codeFlow = require("../../src/services/kdbInsights/codeFlowLogin"); +const dummyMeta: MetaObject = { + header: { + ac: "0", + agg: ":127.0.0.1:5070", + ai: "", + api: ".kxi.getMeta", + client: ":127.0.0.1:5050", + corr: "CorrHash", + http: "json", + logCorr: "logCorrHash", + protocol: "gw", + rc: "0", + rcvTS: "2099-05-22T11:06:33.650000000", + retryCount: "0", + to: "2099-05-22T11:07:33.650000000", + userID: "dummyID", + userName: "testUser", + }, + payload: { + rc: [ + { + api: 3, + agg: 1, + assembly: 1, + schema: 1, + rc: "dummy-rc", + labels: [{ kxname: "dummy-assembly" }], + started: "2023-10-04T17:20:57.659088747", + }, + ], + dap: [], + api: [], + agg: [ + { + aggFn: ".sgagg.aggFnDflt", + custom: false, + full: true, + metadata: { + description: "dummy desc.", + params: [{ description: "dummy desc." }], + return: { description: "dummy desc." }, + misc: {}, + }, + procs: [], + }, + ], + assembly: [ + { + assembly: "dummy-assembly", + kxname: "dummy-assembly", + tbls: ["dummyTbl"], + }, + ], + schema: [], + }, +}; + describe("kdbTreeProvider", () => { let servers: Server; let insights: Insights; @@ -544,6 +604,112 @@ describe("kdbTreeProvider", () => { "QServer node creation failed", ); }); + + describe("InsightsMetaNode", () => { + it("should initialize fields correctly", () => { + const node = new InsightsMetaNode( + ["child1", "child2"], + "testLabel", + "testDetails", + TreeItemCollapsibleState.Collapsed, + "testConnLabel", + ); + + assert.deepStrictEqual(node.children, ["child1", "child2"]); + assert.strictEqual(node.label, "testLabel"); + assert.strictEqual( + node.collapsibleState, + TreeItemCollapsibleState.Collapsed, + ); + assert.strictEqual(node.connLabel, "testConnLabel"); + assert.strictEqual(node.description, ""); + assert.strictEqual(node.contextValue, "meta"); + }); + + it("should return empty string from getDescription", () => { + const node = new InsightsMetaNode( + [], + "", + "", + TreeItemCollapsibleState.None, + "", + ); + + assert.strictEqual(node.getDescription(), ""); + }); + }); + + describe("MetaObjectPayloadNode", () => { + it("should initialize fields correctly", () => { + const node = new MetaObjectPayloadNode( + ["child1", "child2"], + "testLabel", + "testDetails", + TreeItemCollapsibleState.Collapsed, + "testIcon", + "testConnLabel", + ); + + assert.deepStrictEqual(node.children, ["child1", "child2"]); + assert.strictEqual(node.label, "testLabel"); + assert.strictEqual( + node.collapsibleState, + TreeItemCollapsibleState.Collapsed, + ); + assert.strictEqual(node.coreIcon, "testIcon"); + assert.strictEqual(node.connLabel, "testConnLabel"); + assert.strictEqual(node.description, ""); + }); + }); + + describe("getChildren", () => { + const kdbProvider = new KdbTreeProvider(servers, insights); + insights = { + testInsight: { + alias: "testInsightsAlias", + server: "testInsightsName", + auth: false, + }, + }; + insightNode = new InsightsNode( + ["child1"], + "testInsight", + insights["testInsight"], + TreeItemCollapsibleState.None, + ); + insightNode.contextValue = "testInsight"; + + afterEach(() => { + ext.kdbinsightsNodes.length = 0; + sinon.restore(); + }); + + it("Should return categories for insights connection", async () => { + ext.kdbinsightsNodes.push("testInsight"); + kdbProvider.getChildren(insightNode); + const result = await kdbProvider.getChildren(insightNode); + assert.notStrictEqual(result, undefined); + }); + + it("should return metaObjects for parent", async () => { + const connMng = new ConnectionManagementService(); + const metaNode = new InsightsMetaNode( + [], + "testMeta", + "", + TreeItemCollapsibleState.None, + "insightsConn", + ); + const insightsConn = new InsightsConnection( + insightNode.label, + insightNode, + ); + sinon.stub(connMng, "retrieveConnectedConnection").returns(insightsConn); + insightsConn.meta = dummyMeta; + const result = await kdbProvider.getChildren(metaNode); + assert.notStrictEqual(result, undefined); + }); + }); }); describe("Code flow login service tests", () => { @@ -560,13 +726,13 @@ describe("Code flow login service tests", () => { it("Should return a correct login", async () => { sinon.stub(codeFlow, "signIn").returns(token); - const result = await signIn("http://localhost"); + const result = await signIn("http://localhost", "insights"); assert.strictEqual(result, token, "Invalid token returned"); }); it("Should execute a correct logout", async () => { sinon.stub(axios, "post").resolves(Promise.resolve({ data: token })); - const result = await signOut("http://localhost", "token"); + const result = await signOut("http://localhost", "insights", "token"); assert.strictEqual(result, undefined, "Invalid response from logout"); }); @@ -574,6 +740,7 @@ describe("Code flow login service tests", () => { sinon.stub(axios, "post").resolves(Promise.resolve({ data: token })); const result = await refreshToken( "http://localhost", + "insights", JSON.stringify(token), ); assert.strictEqual( @@ -584,7 +751,7 @@ describe("Code flow login service tests", () => { }); it("Should not return token from secret store", async () => { - const result = await getCurrentToken("", "testalias"); + const result = await getCurrentToken("", "testalias", "insights"); assert.strictEqual( result, undefined, @@ -593,7 +760,7 @@ describe("Code flow login service tests", () => { }); it("Should not return token from secret store", async () => { - const result = await getCurrentToken("testserver", ""); + const result = await getCurrentToken("testserver", "", "insights"); assert.strictEqual( result, undefined, @@ -601,9 +768,11 @@ describe("Code flow login service tests", () => { ); }); - it.skip("Should not sign in if link is not opened", async () => { - sinon.stub(env, "openExternal").value(async () => false); - await assert.rejects(() => signIn("http://127.0.0.1")); + it("Should continue sign in if link is copied", async () => { + sinon.stub(env, "openExternal").value(async () => { + throw new Error(); + }); + await assert.rejects(() => signIn("http://127.0.0.1", "insights")); }); }); @@ -1085,6 +1254,107 @@ describe("connectionManagerService", () => { sinon.assert.calledOnce(getMetaStub); }); }); + + describe("getMetaInfoType", () => { + it("should return correct MetaInfoType for valid input", () => { + assert.strictEqual( + connectionManagerService.getMetaInfoType("meta".toUpperCase()), + MetaInfoType.META, + ); + assert.strictEqual( + connectionManagerService.getMetaInfoType("schema".toUpperCase()), + MetaInfoType.SCHEMA, + ); + assert.strictEqual( + connectionManagerService.getMetaInfoType("api".toUpperCase()), + MetaInfoType.API, + ); + assert.strictEqual( + connectionManagerService.getMetaInfoType("agg".toUpperCase()), + MetaInfoType.AGG, + ); + assert.strictEqual( + connectionManagerService.getMetaInfoType("dap".toUpperCase()), + MetaInfoType.DAP, + ); + assert.strictEqual( + connectionManagerService.getMetaInfoType("rc".toUpperCase()), + MetaInfoType.RC, + ); + }); + + it("should return undefined for invalid input", () => { + assert.strictEqual( + connectionManagerService.getMetaInfoType("invalid"), + undefined, + ); + }); + }); + + describe("retrieveMetaContent", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should return empty string for invalid meta info type", () => { + sandbox + .stub(connectionManagerService, "getMetaInfoType") + .returns(undefined); + assert.strictEqual( + connectionManagerService.retrieveMetaContent("connLabel", "invalid"), + "", + ); + }); + + it("should return empty string for not connected connection", () => { + sandbox + .stub(connectionManagerService, "getMetaInfoType") + .returns(MetaInfoType.META); + sandbox + .stub(connectionManagerService, "retrieveConnectedConnection") + .returns(undefined); + assert.strictEqual( + connectionManagerService.retrieveMetaContent("connLabel", "meta"), + "", + ); + }); + + it("should return empty string for local connection", () => { + sandbox + .stub(connectionManagerService, "getMetaInfoType") + .returns(MetaInfoType.META); + sandbox + .stub(connectionManagerService, "retrieveConnectedConnection") + .returns(localConn); + assert.strictEqual( + connectionManagerService.retrieveMetaContent("connLabel", "meta"), + "", + ); + }); + + it("should return meta object for valid input", () => { + insightsConn.meta = dummyMeta; + sandbox + .stub(connectionManagerService, "getMetaInfoType") + .returns(MetaInfoType.META); + sandbox + .stub(connectionManagerService, "retrieveConnectedConnection") + .returns(insightsConn); + assert.strictEqual( + connectionManagerService.retrieveMetaContent( + insightsConn.connLabel, + "meta", + ), + JSON.stringify(dummyMeta.payload), + ); + }); + }); }); describe("dataSourceEditorProvider", () => { @@ -1412,3 +1682,37 @@ describe("CompletionProvider", () => { assert.ok(items); }); }); + +describe("MetaContentProvider", () => { + let metaContentProvider: MetaContentProvider; + let uri: Uri; + + beforeEach(() => { + metaContentProvider = new MetaContentProvider(); + uri = Uri.parse("foo://example.com"); + }); + + it("should update content and fire onDidChange event", () => { + const content = "new content"; + const spy = sinon.spy(); + + metaContentProvider.onDidChange(spy); + + metaContentProvider.update(uri, content); + + assert.strictEqual( + metaContentProvider.provideTextDocumentContent(uri), + content, + ); + assert(spy.calledOnceWith(uri)); + }); + + it("should provide text document content", () => { + const content = "content"; + metaContentProvider.update(uri, content); + assert.strictEqual( + metaContentProvider.provideTextDocumentContent(uri), + content, + ); + }); +}); diff --git a/test/suite/utils.test.ts b/test/suite/utils.test.ts index 4ebbc944..3572807c 100644 --- a/test/suite/utils.test.ts +++ b/test/suite/utils.test.ts @@ -25,6 +25,7 @@ import { InsightsNode, KdbNode } from "../../src/services/kdbTreeProvider"; import { QueryHistoryProvider } from "../../src/services/queryHistoryProvider"; import { KdbResultsViewProvider } from "../../src/services/resultsPanelProvider"; import * as coreUtils from "../../src/utils/core"; +import * as cpUtils from "../../src/utils/cpUtils"; import * as dataSourceUtils from "../../src/utils/dataSource"; import * as decodeUtils from "../../src/utils/decode"; import * as executionUtils from "../../src/utils/execution"; @@ -70,6 +71,36 @@ describe("Utils", () => { }); describe("core", () => { + describe("checkOpenSslInstalled", () => { + let tryExecuteCommandStub: sinon.SinonStub; + let kdbOutputLogStub: sinon.SinonStub; + beforeEach(() => { + tryExecuteCommandStub = sinon.stub(cpUtils, "tryExecuteCommand"); + kdbOutputLogStub = sinon.stub(coreUtils, "kdbOutputLog"); + }); + + afterEach(() => { + tryExecuteCommandStub.restore(); + kdbOutputLogStub.restore(); + }); + + it("should return null if OpenSSL is not installed", async () => { + tryExecuteCommandStub.resolves({ code: 1, cmdOutput: "" }); + + const result = await coreUtils.checkOpenSslInstalled(); + + assert.strictEqual(result, null); + }); + + it("should handle errors correctly", async () => { + const error = new Error("Test error"); + tryExecuteCommandStub.rejects(error); + + const result = await coreUtils.checkOpenSslInstalled(); + + assert.strictEqual(result, null); + }); + }); describe("setOutputWordWrapper", () => { let getConfigurationStub: sinon.SinonStub; beforeEach(() => { @@ -250,6 +281,55 @@ describe("Utils", () => { assert.strictEqual(result, ""); }); }); + + describe("coreLogs", () => { + ext.outputChannel = vscode.window.createOutputChannel("kdb"); + let appendLineSpy, showErrorMessageSpy: sinon.SinonSpy; + beforeEach(() => { + appendLineSpy = sinon.spy( + vscode.window.createOutputChannel("testChannel"), + "appendLine", + ); + showErrorMessageSpy = sinon.spy(vscode.window, "showErrorMessage"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("kdbOutputLog should log message with date and type", () => { + const message = "test message"; + const type = "INFO"; + + coreUtils.kdbOutputLog(message, type); + + appendLineSpy.calledOnce; + appendLineSpy.calledWithMatch(message); + appendLineSpy.calledWithMatch(type); + }); + + it("tokenUndefinedError should log and show error message", () => { + const connLabel = "test connection"; + + coreUtils.tokenUndefinedError(connLabel); + + appendLineSpy.calledOnce; + showErrorMessageSpy.calledOnce; + appendLineSpy.calledWithMatch(connLabel); + showErrorMessageSpy.calledWithMatch(connLabel); + }); + + it("invalidUsernameJWT should log and show error message", () => { + const connLabel = "test connection"; + + coreUtils.invalidUsernameJWT(connLabel); + + appendLineSpy.calledOnce; + showErrorMessageSpy.calledOnce; + appendLineSpy.calledWithMatch(connLabel); + showErrorMessageSpy.calledWithMatch(connLabel); + }); + }); }); describe("dataSource", () => { @@ -267,6 +347,11 @@ describe("Utils", () => { assert.strictEqual(result, "2021-01-01T00:00:00.000000000"); }); + it("convertTimeToTimestamp", () => { + const result = dataSourceUtils.convertTimeToTimestamp("testTime"); + assert.strictEqual(result, ""); + }); + it("getConnectedInsightsNode", () => { const result = dataSourceUtils.getConnectedInsightsNode(); assert.strictEqual(result, ""); @@ -899,6 +984,11 @@ describe("Utils", () => { }); describe("handleWSResults", () => { + afterEach(() => { + sinon.restore(); + ext.isResultsTabVisible = false; + }); + it("should return no results found", () => { const ab = new ArrayBuffer(128); const result = queryUtils.handleWSResults(ab); @@ -914,18 +1004,14 @@ describe("Utils", () => { rows: [{ Value: "10" }], }; const uriTest: vscode.Uri = vscode.Uri.parse("test"); - ext.resultsViewProvider = new KdbResultsViewProvider(uriTest); + ext.isResultsTabVisible = true; const qtableStub = sinon .stub(QTable.default, "toLegacy") .returns(expectedOutput); - const isVisibleStub = sinon - .stub(ext.resultsViewProvider, "isVisible") - .returns(true); const convertRowsSpy = sinon.spy(queryUtils, "convertRows"); const result = queryUtils.handleWSResults(ab); sinon.assert.notCalled(convertRowsSpy); assert.strictEqual(result, expectedOutput); - sinon.restore(); }); }); diff --git a/test/suite/webview.test.ts b/test/suite/webview.test.ts index b888ab4e..740d8948 100644 --- a/test/suite/webview.test.ts +++ b/test/suite/webview.test.ts @@ -479,6 +479,7 @@ describe("KdbNewConnectionView", () => { alias: "", server: "", auth: true, + realm: "", }; const data = view["data"]; assert.deepEqual(data, expectedData);