From db9c41ce16c8c541e809b26fe787245d17f1633f Mon Sep 17 00:00:00 2001 From: ecmel Date: Mon, 22 Dec 2025 13:18:56 +0300 Subject: [PATCH 1/8] update build --- .vscodeignore | 3 +- esbuild.mjs | 96 ++++++++++++++++++++++++----------------------- eslint.config.mjs | 6 +-- package.json | 4 +- tsconfig.json | 6 +-- 5 files changed, 58 insertions(+), 57 deletions(-) diff --git a/.vscodeignore b/.vscodeignore index 04a0de2c..1db9423f 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -11,7 +11,8 @@ node_modules server test/ img/ -esbuild.js +qcumber.sh +esbuild.mjs sonar-project.properties eslint.config.cjs eslint.config.mjs diff --git a/esbuild.mjs b/esbuild.mjs index 5be5420e..11478308 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -1,14 +1,4 @@ import { context, build } from "esbuild"; -import { copyFileSync, mkdirSync } from "fs"; -import { sync } from "glob"; -import { join, basename } from "path"; - -function copyFiles(srcPattern, destDir) { - sync(srcPattern).forEach((file) => { - const destFile = join(destDir, basename(file)); - copyFileSync(file, destFile); - }); -} const minify = process.argv.includes("--minify"); const sourcemap = process.argv.includes("--sourcemap"); @@ -22,59 +12,71 @@ const baseConfig = { bundle: true, }; -const extensionConfig = { +const cssConfig = { ...baseConfig, - outfile: "./out/extension.js", - entryPoints: ["./src/extension.ts"], + entryPoints: [ + "./src/webview/styles/style.css", + "./src/webview/styles/light.css", + ], + outdir: "./out", +}; + +const webviewConfig = { + ...baseConfig, + entryPoints: ["./src/webview/main.ts"], + outfile: "./out/webview.js", + format: "esm", + target: "es2020", external: ["vscode"], - format: "cjs", - platform: "node", }; const serverConfig = { ...baseConfig, - outfile: "./out/server.js", entryPoints: ["./server/src/server.ts"], + outfile: "./out/server.js", format: "cjs", - external: ["vscode"], platform: "node", + external: ["vscode"], }; -const webviewConfig = { +const extensionConfig = { ...baseConfig, - target: "es2020", - format: "esm", - entryPoints: ["./src/webview/main.ts"], + entryPoints: ["./src/extension.ts"], + outfile: "./out/extension.js", + format: "cjs", + platform: "node", external: ["vscode"], - outfile: "./out/webview.js", }; -(async () => { - try { - mkdirSync("./out", { recursive: true }); - copyFiles("src/webview/styles/*.css", "./out"); - - if (watch) { - console.log("esbuild:started"); - const contexts = await Promise.all([ - context(serverConfig), - context(webviewConfig), - context(extensionConfig), - ]); - await Promise.all(contexts.map((ctx) => ctx.rebuild())).finally(() => +if (watch) { + console.log("esbuild:started"); + Promise.all([ + context(cssConfig), + context(webviewConfig), + context(serverConfig), + context(extensionConfig), + ]) + .then((contexts) => + Promise.all(contexts.map((ctx) => ctx.rebuild())).finally(() => Promise.all(contexts.map((ctx) => ctx.watch({ delay: 500 }))) .then(() => console.log("esbuild:watching")) - .catch((err) => { - console.error(err); + .catch((error) => { + console.error(error); process.exit(1); }), - ); - } else { - await build(serverConfig); - await build(webviewConfig); - await build(extensionConfig); - } - } catch (err) { - console.error(err); - } -})(); + ), + ) + .catch((error) => { + console.error(error); + }); +} else { + Promise.all([ + build(cssConfig), + build(webviewConfig), + build(serverConfig), + build(extensionConfig), + ]).catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/eslint.config.mjs b/eslint.config.mjs index 881ebce2..5127f37c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -8,16 +8,14 @@ import * as tseslint from "typescript-eslint"; const currentYear = new Date().getFullYear(); export default [ - { - ignores: ["**/*.d.ts", "**/*.js", "**/*.mjs", "src/ipc/**", "test/fixtures/**"], - }, + { ignores: ["**/*.d.ts", "**/*.js", "**/*.mjs", "src/ipc/**/*"] }, js.configs.recommended, ...tseslint.configs.recommended, { languageOptions: { parser: tseslint.parser, parserOptions: { - ecmaVersion: 2018, + ecmaVersion: 2020, sourceType: "module", }, }, diff --git a/package.json b/package.json index b30edf02..9d437b49 100644 --- a/package.json +++ b/package.json @@ -1201,8 +1201,8 @@ }, "scripts": { "update-deps": "ncu --target patch -u", - "format": "prettier --write \"**/*.+(js|ts)\"", - "lint": "eslint $(git ls-files '*.ts') --fix --no-warn-ignored", + "format": "prettier --write \"**/*.ts\"", + "lint": "eslint --fix --no-warn-ignored", "esbuild-base": "rimraf out && node ./esbuild.mjs", "watch": "npm run -S esbuild-base -- --sourcemap --watch", "build": "npm run -S esbuild-base -- --sourcemap", diff --git a/tsconfig.json b/tsconfig.json index 63f37056..07de62b4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { "module": "commonjs", - "target": "ES2019", - "outDir": "out", - "lib": ["ES2019", "WebWorker", "DOM", "DOM.Iterable"], + "target": "ES2020", + "lib": ["ES2020", "WebWorker", "DOM", "DOM.Iterable"], + "isolatedModules": true, "sourceMap": true, "strict": true, "noFallthroughCasesInSwitch": true, From 09c5d2fdcccf709f50697ea0ab1025255f592f2d Mon Sep 17 00:00:00 2001 From: ecmel Date: Wed, 24 Dec 2025 15:00:39 +0300 Subject: [PATCH 2/8] added input picker widget --- src/commands/workspaceCommand.ts | 3 +- src/utils/widgets.ts | 49 +++++++++++ test/suite/commands/workspaceCommand.test.ts | 5 +- test/suite/utils/widgets.test.ts | 87 ++++++++++++++++++++ 4 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 src/utils/widgets.ts create mode 100644 test/suite/utils/widgets.test.ts diff --git a/src/commands/workspaceCommand.ts b/src/commands/workspaceCommand.ts index 2d0b70d1..ab707d5d 100644 --- a/src/commands/workspaceCommand.ts +++ b/src/commands/workspaceCommand.ts @@ -58,6 +58,7 @@ import { errorMessage, normalizeAssemblyTarget, } from "../utils/shared"; +import { showInputPicker } from "../utils/widgets"; const logger = "workspaceCommand"; @@ -228,7 +229,7 @@ export async function pickConnection(uri: Uri) { const servers = getServers(); const items = ["(none)", ...servers]; - let picked = await window.showQuickPick(items, { + let picked = await showInputPicker(items, { title: `Choose Connection (${getBasename(uri)})`, placeHolder: server, }); diff --git a/src/utils/widgets.ts b/src/utils/widgets.ts new file mode 100644 index 00000000..71139532 --- /dev/null +++ b/src/utils/widgets.ts @@ -0,0 +1,49 @@ +/* + * Copyright (c) 1998-2025 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 { QuickPickOptions, window } from "vscode"; + +export function showInputPicker( + items: readonly string[], + options: QuickPickOptions, +) { + return new Promise((resolve) => { + const picker = window.createQuickPick(); + + picker.items = items.map((item) => ({ label: item })); + picker.placeholder = options.placeHolder; + picker.title = options.title; + + let selected = ""; + let accepted = false; + + picker.onDidChangeValue((value) => { + selected = value; + }); + + picker.onDidChangeSelection((item) => { + selected = item[0].label; + }); + + picker.onDidAccept(() => { + accepted = true; + picker.hide(); + }); + + picker.onDidHide(() => { + resolve((accepted && selected) || undefined); + }); + + picker.show(); + }); +} diff --git a/test/suite/commands/workspaceCommand.test.ts b/test/suite/commands/workspaceCommand.test.ts index ed7cef61..576db373 100644 --- a/test/suite/commands/workspaceCommand.test.ts +++ b/test/suite/commands/workspaceCommand.test.ts @@ -26,6 +26,7 @@ import { WorkspaceTreeProvider } from "../../../src/services/workspaceTreeProvid import * as dataSourceUtils from "../../../src/utils/dataSource"; import * as loggers from "../../../src/utils/loggers"; import * as notifications from "../../../src/utils/notifications"; +import * as widgets from "../../../src/utils/widgets"; describe("workspaceCommand", () => { const kdbUri = vscode.Uri.file("test-kdb.q"); @@ -153,7 +154,7 @@ describe("workspaceCommand", () => { describe("pickConnection", () => { it("should pick from available servers", async () => { - sinon.stub(vscode.window, "showQuickPick").value(async () => "test"); + sinon.stub(widgets, "showInputPicker").value(async () => "test"); const result = await workspaceCommand.pickConnection( vscode.Uri.file("test.kdb.q"), ); @@ -161,7 +162,7 @@ describe("workspaceCommand", () => { }); it("should return undefined from (none)", async () => { - sinon.stub(vscode.window, "showQuickPick").value(async () => "(none)"); + sinon.stub(widgets, "showInputPicker").value(async () => "(none)"); const result = await workspaceCommand.pickConnection( vscode.Uri.file("test.kdb.q"), ); diff --git a/test/suite/utils/widgets.test.ts b/test/suite/utils/widgets.test.ts new file mode 100644 index 00000000..6e5652f8 --- /dev/null +++ b/test/suite/utils/widgets.test.ts @@ -0,0 +1,87 @@ +/* + * Copyright (c) 1998-2025 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 assert from "assert"; +import * as sinon from "sinon"; +import * as vscode from "vscode"; + +import * as widgets from "../../../src/utils/widgets"; + +describe("Widgets", () => { + describe("showInputPicker", () => { + let onDidChangeValue: any; + let onDidChangeSelection: any; + let onDidAccept: any; + let onDidHide: any; + + const picker = { + show() {}, + hide() {}, + onDidChangeValue(listener: any) { + onDidChangeValue = listener; + }, + onDidChangeSelection(listener: any) { + onDidChangeSelection = listener; + }, + onDidAccept(listener: any) { + onDidAccept = listener; + }, + onDidHide(listener: any) { + onDidHide = listener; + }, + }; + + const items = ["first", "second"]; + + const options = { + placeHolder: "pick", + title: "Picker", + }; + + beforeEach(() => { + sinon.stub(vscode.window, "createQuickPick").returns(picker); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return undefined", async () => { + setTimeout(() => { + onDidHide(); + }); + const res = await widgets.showInputPicker(items, options); + assert.strictEqual(res, undefined); + }); + + it("should return input", async () => { + setTimeout(() => { + onDidChangeValue(items[0]); + onDidAccept(); + onDidHide(); + }); + const res = await widgets.showInputPicker(items, options); + assert.strictEqual(res, items[0]); + }); + + it("should return selection", async () => { + setTimeout(() => { + onDidChangeSelection([{ label: items[1] }]); + onDidAccept(); + onDidHide(); + }); + const res = await widgets.showInputPicker(items, options); + assert.strictEqual(res, items[1]); + }); + }); +}); From e0fa52805475552493672b56016df85e35a7ea5e Mon Sep 17 00:00:00 2001 From: ecmel Date: Sun, 28 Dec 2025 18:35:11 +0300 Subject: [PATCH 3/8] added quick connections --- ref_card.md | 18 ++++++------ src/commands/serverCommand.ts | 33 +++++++++++++++++++++ src/commands/workspaceCommand.ts | 34 ++++++++++++++++----- src/services/notebookController.ts | 3 +- src/utils/core.ts | 8 +++++ test/suite/commands/serverCommand.test.ts | 36 +++++++++++++++++++++++ 6 files changed, 114 insertions(+), 18 deletions(-) diff --git a/ref_card.md b/ref_card.md index 8e6d6c2b..ba10a71f 100644 --- a/ref_card.md +++ b/ref_card.md @@ -135,15 +135,15 @@ | Run.Cell.ie.dap.q | | src/utils/queryUtils.ts | | Run.Cell.ie.dap.py | | src/utils/queryUtils.ts | | ¦ | | | -| Run.Workbook.kdb.quick.q | | | -| Run.Workbook.kdb.quick.py | | | -| Run.Workbook.kdb.quick.sql | | | -| Run.File.kdb.quick.q | | | -| Run.File.kdb.quick.py | | | -| Run.File.kdb.quick.sql | | | -| Run.Cell.kdb.quick.q | | | -| Run.Cell.kdb.quick.py | | | -| Run.Cell.kdb.quick.sql | | | +| Run.Workbook.kdb.quick.q | | src/utils/queryUtils.ts | +| Run.Workbook.kdb.quick.py | | src/utils/queryUtils.ts | +| Run.Workbook.kdb.quick.sql | | src/utils/queryUtils.ts | +| Run.File.kdb.quick.q | | src/utils/queryUtils.ts | +| Run.File.kdb.quick.py | | src/utils/queryUtils.ts | +| Run.File.kdb.quick.sql | | src/utils/queryUtils.ts | +| Run.Cell.kdb.quick.q | | src/utils/queryUtils.ts | +| Run.Cell.kdb.quick.py | | src/utils/queryUtils.ts | +| Run.Cell.kdb.quick.sql | | src/utils/queryUtils.ts | | ¦ | | | | Run.Datasource.api | | src/utils/queryUtils.ts | | Run.Datasource.qsql | | src/utils/queryUtils.ts | diff --git a/src/commands/serverCommand.ts b/src/commands/serverCommand.ts index df0e1d50..4ba359cc 100644 --- a/src/commands/serverCommand.ts +++ b/src/commands/serverCommand.ts @@ -1429,3 +1429,36 @@ function isValidExportedConnections(data: any): data is ExportedConnections { Array.isArray(data.connections.KDB) ); } + +const quickConnections: ServerDetails[] = []; + +export async function ensureQuickConnection(server: string) { + const [host, port, user] = server.split(":"); + + let connection = quickConnections.find( + (conn) => + conn.serverName === host && + conn.serverPort === port && + conn.username === user, + ); + + if (!connection) { + const serverAlias = "(Connection " + (quickConnections.length + 1) + ")"; + const authData = await ext.secretSettings.getAuthData(server); + if (authData) await ext.secretSettings.storeAuthData(serverAlias, authData); + connection = { + serverAlias, + serverName: host, + serverPort: port, + username: user, + auth: !!authData, + tls: false, + }; + quickConnections.push(connection); + const servers = getServers(); + quickConnections.forEach((conn) => (servers[conn.serverAlias] = conn)); + ext.serverProvider.refresh(servers); + } + + return connection.serverAlias; +} diff --git a/src/commands/workspaceCommand.ts b/src/commands/workspaceCommand.ts index ab707d5d..06988955 100644 --- a/src/commands/workspaceCommand.ts +++ b/src/commands/workspaceCommand.ts @@ -29,7 +29,11 @@ import { } from "vscode"; import { ext } from "../extensionVariables"; -import { resetScratchpad, runQuery } from "./serverCommand"; +import { + ensureQuickConnection, + resetScratchpad, + runQuery, +} from "./serverCommand"; import { InsightsConnection } from "../classes/insightsConnection"; import { LocalConnection } from "../classes/localConnection"; import { ReplConnection } from "../classes/replConnection"; @@ -38,7 +42,12 @@ import { MetaDap } from "../models/meta"; import { ConnectionManagementService } from "../services/connectionManagerService"; import { InsightsNode, KdbNode, LabelNode } from "../services/kdbTreeProvider"; import { updateCellMetadata } from "../services/notebookProviders"; -import { getBasename, offerConnectAction } from "../utils/core"; +import { + getBasename, + isQuick, + isQuickAlias, + offerConnectAction, +} from "../utils/core"; import { importOldDsFiles } from "../utils/dataSource"; import { Cancellable, @@ -190,7 +199,8 @@ export function getServerForUri(uri: Uri) { const server = map[relativePath(uri)]; const servers = getServers(); - return server && (server === ext.REPL || servers.includes(server)) + return isQuick(server) || + (server && (server === ext.REPL || servers.includes(server))) ? server : undefined; } @@ -240,6 +250,15 @@ export async function pickConnection(uri: Uri) { await setTargetForUri(uri, undefined); } if (picked) { + if (isQuick(picked)) { + const [host, port, user, pass] = picked.split(":"); + if (user) { + picked = host + ":" + port + ":" + user; + await ext.secretSettings.storeAuthData(picked, `${user}:${pass}`); + } else { + picked = host + ":" + port; + } + } setRunScratchpadItemText(uri, picked); ext.runScratchpadItem.show(); } else { @@ -589,6 +608,7 @@ export async function runActiveEditor(type?: ExecutionTypes) { (type === ExecutionTypes.PopulateScratchpad ? 0 : RunFlag.Run) | (isInsights ? RunFlag.Insights : 0) | (target ? RunFlag.Dap : 0) | + (isQuickAlias(conn.connLabel) ? RunFlag.Quick : 0) | (isWorkbook(uri) ? RunFlag.Workbook : 0) | (isPython(uri) ? RunFlag.Python : 0) | (isSql(uri) ? RunFlag.Sql : 0), @@ -642,14 +662,12 @@ export class ConnectionLensProvider implements CodeLensProvider { title: server ? `Run on ${server}` : "Choose Connection", }); - const target = getTargetForUri(document.uri); - if (server) { const conn = await getConnectionForServer(server); if (!isSql(document.uri) && conn instanceof InsightsNode) { const pickTarget = new CodeLens(top, { command: "kdb.file.pickTarget", - title: target || "scratchpad", + title: getTargetForUri(document.uri) || "scratchpad", }); return [pickConnection, pickTarget]; } @@ -737,14 +755,14 @@ export async function importOldDSFiles() { export async function findConnection(uri: Uri) { /* c8 ignore start */ - const connMngService = new ConnectionManagementService(); - let conn: InsightsConnection | LocalConnection | undefined; let server = getServerForUri(uri) ?? ""; if (server) { + if (isQuick(server)) server = await ensureQuickConnection(server); const node = await getConnectionForServer(server); if (node) { + const connMngService = new ConnectionManagementService(); server = node.label; conn = connMngService.retrieveConnectedConnection(server); if (conn === undefined) { diff --git a/src/services/notebookController.ts b/src/services/notebookController.ts index 457d0174..1c7689ab 100644 --- a/src/services/notebookController.ts +++ b/src/services/notebookController.ts @@ -30,7 +30,7 @@ import { } from "../commands/workspaceCommand"; import { ext } from "../extensionVariables"; import { CellKind } from "../models/notebook"; -import { getBasename } from "../utils/core"; +import { getBasename, isQuickAlias } from "../utils/core"; import { MessageKind, notify } from "../utils/notifications"; import { resultToBase64, @@ -177,6 +177,7 @@ export class KxNotebookController { (variable ? 0 : RunFlag.Run) | (isInsights ? RunFlag.Insights : 0) | (target ? RunFlag.Dap : 0) | + (isQuickAlias(conn.connLabel) ? RunFlag.Quick : 0) | (kind === CellKind.PYTHON ? RunFlag.Python : 0) | (kind === CellKind.SQL ? RunFlag.Sql : 0), ); diff --git a/src/utils/core.ts b/src/utils/core.ts index c285bbe3..5fd9def4 100644 --- a/src/utils/core.ts +++ b/src/utils/core.ts @@ -684,3 +684,11 @@ export function isBaseVersionGreaterOrEqual( export function getBasename(uri: Uri): string { return path.basename(uri.path); } + +export function isQuick(server: string | undefined) { + return server?.includes(":"); +} + +export function isQuickAlias(alias: string | undefined) { + return alias?.startsWith("("); +} diff --git a/test/suite/commands/serverCommand.test.ts b/test/suite/commands/serverCommand.test.ts index e4053515..3f0d330d 100644 --- a/test/suite/commands/serverCommand.test.ts +++ b/test/suite/commands/serverCommand.test.ts @@ -1555,8 +1555,44 @@ describe("serverCommand", () => { success: true, isDatasource: true, }; + await serverCommand.copyQuery(queryHistory); sinon.assert.called(showInfoStub); }); }); + + describe("ensureQuickConnection", () => { + let secretSettings: any; + let setAuthDataLabel: any; + let setAuthDataAuth: any; + beforeEach(() => { + secretSettings = ext.secretSettings; + ext.secretSettings = { + getAuthData() { + return "test:1234"; + }, + storeAuthData(label: any, auth: any) { + setAuthDataLabel = label; + setAuthDataAuth = auth; + }, + }; + }); + afterEach(() => { + ext.secretSettings = secretSettings; + secretSettings = undefined; + setAuthDataAuth = undefined; + setAuthDataLabel = undefined; + }); + afterEach(() => {}); + it("should create quick connection", async () => { + const label = await serverCommand.ensureQuickConnection("local:1234"); + assert.strictEqual(label, "(Connection 1)"); + }); + it("should create quick connection with auth", async () => { + const label = + await serverCommand.ensureQuickConnection("local:1234:test"); + assert.strictEqual(setAuthDataLabel, label); + assert.strictEqual(setAuthDataAuth, "test:1234"); + }); + }); }); From 5eaf6f7150e7517788d53a468e4cb649775c9139 Mon Sep 17 00:00:00 2001 From: ecmel Date: Sun, 28 Dec 2025 22:23:19 +0300 Subject: [PATCH 4/8] added quick connections selection --- package.json | 10 ++-- src/commands/serverCommand.ts | 15 ++++-- src/commands/workspaceCommand.ts | 60 +++++++++++++++++------- src/services/connectionManagerService.ts | 7 ++- 4 files changed, 64 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 9d437b49..70fa0f5c 100644 --- a/package.json +++ b/package.json @@ -971,7 +971,7 @@ }, { "command": "kdb.connections.edit", - "when": "view == kdb-servers && (viewItem in kdb.rootNodes || viewItem in kdb.insightsNodes)", + "when": "view == kdb-servers && viewItem not in kdb.kdbQuickNodes && (viewItem in kdb.rootNodes || viewItem in kdb.insightsNodes)", "group": "connection@4" }, { @@ -981,12 +981,12 @@ }, { "command": "kdb.connections.addAuthentication", - "when": "view == kdb-servers && viewItem not in kdb.insightsNodes && viewItem in kdb.kdbNodesWithoutAuth && viewItem not in kdb.local", + "when": "view == kdb-servers && viewItem not in kdb.kdbQuickNodes && viewItem not in kdb.insightsNodes && viewItem in kdb.kdbNodesWithoutAuth && viewItem not in kdb.local", "group": "connection@3" }, { "command": "kdb.connections.enableTLS", - "when": "view == kdb-servers && viewItem not in kdb.insightsNodes && viewItem in kdb.kdbNodesWithoutTls && viewItem not in kdb.local", + "when": "view == kdb-servers && viewItem not in kdb.kdbQuickNodes && viewItem not in kdb.insightsNodes && viewItem in kdb.kdbNodesWithoutTls && viewItem not in kdb.local", "group": "connection@4" }, { @@ -1006,12 +1006,12 @@ }, { "command": "kdb.connections.remove.kdb", - "when": "view == kdb-servers && viewItem in kdb.rootNodes", + "when": "view == kdb-servers && viewItem in kdb.rootNodes && viewItem not in kdb.kdbQuickNodes", "group": "connection@5" }, { "command": "kdb.connections.export.single", - "when": "view == kdb-servers && (viewItem in kdb.rootNodes || viewItem in kdb.insightsNodes)", + "when": "view == kdb-servers && viewItem not in kdb.kdbQuickNodes && (viewItem in kdb.rootNodes || viewItem in kdb.insightsNodes)", "group": "connection@6" }, { diff --git a/src/commands/serverCommand.ts b/src/commands/serverCommand.ts index 4ba359cc..73123c43 100644 --- a/src/commands/serverCommand.ts +++ b/src/commands/serverCommand.ts @@ -1444,19 +1444,28 @@ export async function ensureQuickConnection(server: string) { if (!connection) { const serverAlias = "(Connection " + (quickConnections.length + 1) + ")"; - const authData = await ext.secretSettings.getAuthData(server); - if (authData) await ext.secretSettings.storeAuthData(serverAlias, authData); + if (user) { + const auth = await ext.secretSettings.getAuthData(server); + if (auth) await ext.secretSettings.storeAuthData(serverAlias, auth); + } connection = { serverAlias, serverName: host, serverPort: port, username: user, - auth: !!authData, + auth: !!user, tls: false, }; quickConnections.push(connection); const servers = getServers(); quickConnections.forEach((conn) => (servers[conn.serverAlias] = conn)); + await commands.executeCommand( + "setContext", + "kdb.kdbQuickNodes", + quickConnections.map( + (conn) => `${conn.serverAlias} [${conn.serverName}:${conn.serverPort}]`, + ), + ); ext.serverProvider.refresh(servers); } diff --git a/src/commands/workspaceCommand.ts b/src/commands/workspaceCommand.ts index 06988955..b111e505 100644 --- a/src/commands/workspaceCommand.ts +++ b/src/commands/workspaceCommand.ts @@ -131,6 +131,21 @@ function getServers() { ]; } +const quickServers: string[] = []; + +function getQuickServers(uri: Uri) { + const conf = workspace.getConfiguration("kdb", uri); + const connections = conf.get<{ [key: string]: string }>("connectionMap", {}); + const size = quickServers.length; + Object.keys(connections).forEach((key) => { + const target = connections[key]; + if (target.includes(":") && !quickServers.includes(target)) { + quickServers.push(target); + } + }); + return size !== quickServers.length ? quickServers.sort() : quickServers; +} + export async function getConnectionForServer( server: string, ): Promise { @@ -236,36 +251,45 @@ export function getConnectionForUri(uri: Uri) { export async function pickConnection(uri: Uri) { /* c8 ignore start */ const server = getServerForUri(uri); - const servers = getServers(); - const items = ["(none)", ...servers]; + const items = ["(none)", ...getServers(), ...getQuickServers(uri)]; let picked = await showInputPicker(items, { title: `Choose Connection (${getBasename(uri)})`, placeHolder: server, }); - if (picked) { - if (picked === "(none)") { - picked = undefined; - await setTargetForUri(uri, undefined); - } - if (picked) { - if (isQuick(picked)) { - const [host, port, user, pass] = picked.split(":"); - if (user) { - picked = host + ":" + port + ":" + user; + if (picked === undefined) return undefined; + + if (isQuick(picked)) { + const [host, port, user, pass] = picked.split(":"); + if (host && port && /^\d+$/s.test(port)) { + if (user) { + picked = host + ":" + port + ":" + user; + if (pass) { await ext.secretSettings.storeAuthData(picked, `${user}:${pass}`); - } else { - picked = host + ":" + port; } + } else { + picked = host + ":" + port; } - setRunScratchpadItemText(uri, picked); - ext.runScratchpadItem.show(); } else { - ext.runScratchpadItem.hide(); + notify(`Connection string "${picked}" is not valid.`, MessageKind.ERROR, { + logger, + }); + return undefined; } - await setServerForUri(uri, picked); + } else if (picked === "(none)") { + picked = undefined; + await setTargetForUri(uri, undefined); + } + + if (picked) { + setRunScratchpadItemText(uri, picked); + ext.runScratchpadItem.show(); + } else { + ext.runScratchpadItem.hide(); } + await setServerForUri(uri, picked); + return picked; /* c8 ignore stop */ } diff --git a/src/services/connectionManagerService.ts b/src/services/connectionManagerService.ts index 390b883d..840c7c31 100644 --- a/src/services/connectionManagerService.ts +++ b/src/services/connectionManagerService.ts @@ -32,6 +32,7 @@ import { getServers, updateInsights, updateServers, + isQuickAlias, } from "../utils/core"; import { refreshDataSourcesPanel } from "../utils/dataSource"; import { MessageKind, notify } from "../utils/notifications"; @@ -525,14 +526,16 @@ export class ConnectionManagementService { return ""; } if (connection instanceof KdbNode) { - exportedContent.connections.KDB.push(connection.details); + if (!isQuickAlias(connection.details.serverAlias)) + exportedContent.connections.KDB.push(connection.details); } else { exportedContent.connections.Insights.push(connection.details); } } else { ext.connectionsList.forEach((connection) => { if (connection instanceof KdbNode) { - exportedContent.connections.KDB.push(connection.details); + if (!isQuickAlias(connection.details.serverAlias)) + exportedContent.connections.KDB.push(connection.details); } else { exportedContent.connections.Insights.push(connection.details); } From b7fff06f09ee5ac62d2367f290432d775aaeec22 Mon Sep 17 00:00:00 2001 From: ecmel Date: Mon, 29 Dec 2025 01:10:37 +0300 Subject: [PATCH 5/8] allow entering password for quick connection --- src/commands/serverCommand.ts | 16 ++++++++++++++-- src/commands/workspaceCommand.ts | 7 ++++++- test/suite/commands/workspaceCommand.test.ts | 8 -------- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/commands/serverCommand.ts b/src/commands/serverCommand.ts index 73123c43..3ed374e6 100644 --- a/src/commands/serverCommand.ts +++ b/src/commands/serverCommand.ts @@ -1445,8 +1445,20 @@ export async function ensureQuickConnection(server: string) { if (!connection) { const serverAlias = "(Connection " + (quickConnections.length + 1) + ")"; if (user) { - const auth = await ext.secretSettings.getAuthData(server); - if (auth) await ext.secretSettings.storeAuthData(serverAlias, auth); + let auth = await ext.secretSettings.getAuthData(server); + if (!auth) { + const password = await window.showInputBox({ + password: true, + prompt: `Enter password for ${server}`, + }); + if (password) { + auth = `${user}:${password}`; + await ext.secretSettings.storeAuthData(server, auth); + } else { + throw new Error(`Password not found for ${server}`); + } + } + await ext.secretSettings.storeAuthData(serverAlias, auth); } connection = { serverAlias, diff --git a/src/commands/workspaceCommand.ts b/src/commands/workspaceCommand.ts index b111e505..40b536b9 100644 --- a/src/commands/workspaceCommand.ts +++ b/src/commands/workspaceCommand.ts @@ -254,7 +254,7 @@ export async function pickConnection(uri: Uri) { const items = ["(none)", ...getServers(), ...getQuickServers(uri)]; let picked = await showInputPicker(items, { - title: `Choose Connection (${getBasename(uri)})`, + title: `Choose a connection or enter a quick connection string for ${getBasename(uri)}`, placeHolder: server, }); @@ -280,6 +280,11 @@ export async function pickConnection(uri: Uri) { } else if (picked === "(none)") { picked = undefined; await setTargetForUri(uri, undefined); + } else if (!items.includes(picked)) { + notify(`Connection "${picked}" is not found.`, MessageKind.ERROR, { + logger, + }); + return undefined; } if (picked) { diff --git a/test/suite/commands/workspaceCommand.test.ts b/test/suite/commands/workspaceCommand.test.ts index 576db373..04ac2576 100644 --- a/test/suite/commands/workspaceCommand.test.ts +++ b/test/suite/commands/workspaceCommand.test.ts @@ -153,14 +153,6 @@ describe("workspaceCommand", () => { }); describe("pickConnection", () => { - it("should pick from available servers", async () => { - sinon.stub(widgets, "showInputPicker").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(widgets, "showInputPicker").value(async () => "(none)"); const result = await workspaceCommand.pickConnection( From 0f0dd5cd92285c2b5c0a4428b6a4dd3f214f7cd2 Mon Sep 17 00:00:00 2001 From: ecmel Date: Mon, 29 Dec 2025 10:45:16 +0300 Subject: [PATCH 6/8] fixed password handling --- src/commands/serverCommand.ts | 8 ++------ src/utils/secretStorage.ts | 4 ++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/commands/serverCommand.ts b/src/commands/serverCommand.ts index 3ed374e6..a47c753e 100644 --- a/src/commands/serverCommand.ts +++ b/src/commands/serverCommand.ts @@ -1451,12 +1451,8 @@ export async function ensureQuickConnection(server: string) { password: true, prompt: `Enter password for ${server}`, }); - if (password) { - auth = `${user}:${password}`; - await ext.secretSettings.storeAuthData(server, auth); - } else { - throw new Error(`Password not found for ${server}`); - } + auth = `${user}:${password ?? ""}`; + await ext.secretSettings.storeAuthData(server, auth); } await ext.secretSettings.storeAuthData(serverAlias, auth); } diff --git a/src/utils/secretStorage.ts b/src/utils/secretStorage.ts index a5a5618d..f680adaf 100644 --- a/src/utils/secretStorage.ts +++ b/src/utils/secretStorage.ts @@ -34,4 +34,8 @@ export default class AuthSettings { const result = await this.secretStorage.get(tokenKey); return result; } + + async deleteAuthData(tokenKey: string): Promise { + this.secretStorage.delete(tokenKey); + } } From 3f464024e4d5127fc675a07c747aca9abcdeda51 Mon Sep 17 00:00:00 2001 From: ecmel Date: Mon, 29 Dec 2025 12:26:04 +0300 Subject: [PATCH 7/8] fixed workbook status icons --- src/commands/workspaceCommand.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/commands/workspaceCommand.ts b/src/commands/workspaceCommand.ts index 40b536b9..3de97de8 100644 --- a/src/commands/workspaceCommand.ts +++ b/src/commands/workspaceCommand.ts @@ -239,12 +239,21 @@ export function getTargetForUri(uri: Uri) { export function getConnectionForUri(uri: Uri) { const server = getServerForUri(uri); if (server) { - return ext.connectionsList.find((item) => { - if (item instanceof InsightsNode) { - return item.details.alias === server; - } - return item.details.serverAlias === server; - }) as KdbNode | InsightsNode; + if (isQuick(server)) { + const [host, port, user] = server.split(":"); + return ext.connectionsList.find( + (item) => + item instanceof KdbNode && + host === item.details.serverName && + port === item.details.serverPort && + user === item.details.username, + ); + } + return ext.connectionsList.find((item) => + item instanceof InsightsNode + ? item.details.alias === server + : item.details.serverAlias === server, + ); } } From 4686a004e6b53f23397124d45b07410980546754 Mon Sep 17 00:00:00 2001 From: ecmel Date: Tue, 30 Dec 2025 19:51:29 +0300 Subject: [PATCH 8/8] fixed password change --- src/commands/serverCommand.ts | 58 +++++++++++++++++++++++++------- src/commands/workspaceCommand.ts | 11 +++--- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/src/commands/serverCommand.ts b/src/commands/serverCommand.ts index a47c753e..a198516f 100644 --- a/src/commands/serverCommand.ts +++ b/src/commands/serverCommand.ts @@ -1432,16 +1432,57 @@ function isValidExportedConnections(data: any): data is ExportedConnections { const quickConnections: ServerDetails[] = []; -export async function ensureQuickConnection(server: string) { - const [host, port, user] = server.split(":"); +function getQuickLabel(conn: ServerDetails) { + return `${conn.serverAlias} [${conn.serverName}:${conn.serverPort}]`; +} - let connection = quickConnections.find( +function getQuickDetail(host: string, port: string, user: string) { + return quickConnections.find( (conn) => conn.serverName === host && conn.serverPort === port && conn.username === user, ); +} + +async function removeQuickDetail(conn: ServerDetails) { + quickConnections.splice(quickConnections.indexOf(conn), 1); + const label = getQuickLabel(conn); + ext.connectedConnectionList + .find((conn) => conn.connLabel === label) + ?.disconnect(); + await refreshQuickProvider(); +} + +async function refreshQuickProvider() { + const servers = getServers(); + quickConnections.forEach((conn) => (servers[conn.serverAlias] = conn)); + await commands.executeCommand( + "setContext", + "kdb.kdbQuickNodes", + quickConnections.map((conn) => getQuickLabel(conn)), + ); + ext.serverProvider.refresh(servers); +} +export async function setQuickPassword( + host: string, + port: string, + user: string, + pass: string, +) { + const conn = getQuickDetail(host, port, user); + if (conn) await removeQuickDetail(conn); + await ext.secretSettings.storeAuthData( + `${host}:${port}:${user}`, + `${user}:${pass}`, + ); +} + +export async function ensureQuickConnection(server: string) { + const [host, port, user] = server.split(":"); + + let connection = getQuickDetail(host, port, user); if (!connection) { const serverAlias = "(Connection " + (quickConnections.length + 1) + ")"; if (user) { @@ -1465,16 +1506,7 @@ export async function ensureQuickConnection(server: string) { tls: false, }; quickConnections.push(connection); - const servers = getServers(); - quickConnections.forEach((conn) => (servers[conn.serverAlias] = conn)); - await commands.executeCommand( - "setContext", - "kdb.kdbQuickNodes", - quickConnections.map( - (conn) => `${conn.serverAlias} [${conn.serverName}:${conn.serverPort}]`, - ), - ); - ext.serverProvider.refresh(servers); + await refreshQuickProvider(); } return connection.serverAlias; diff --git a/src/commands/workspaceCommand.ts b/src/commands/workspaceCommand.ts index 3de97de8..deef291f 100644 --- a/src/commands/workspaceCommand.ts +++ b/src/commands/workspaceCommand.ts @@ -33,6 +33,7 @@ import { ensureQuickConnection, resetScratchpad, runQuery, + setQuickPassword, } from "./serverCommand"; import { InsightsConnection } from "../classes/insightsConnection"; import { LocalConnection } from "../classes/localConnection"; @@ -273,15 +274,13 @@ export async function pickConnection(uri: Uri) { const [host, port, user, pass] = picked.split(":"); if (host && port && /^\d+$/s.test(port)) { if (user) { - picked = host + ":" + port + ":" + user; - if (pass) { - await ext.secretSettings.storeAuthData(picked, `${user}:${pass}`); - } + picked = `${host}:${port}:${user}`; + if (pass !== undefined) await setQuickPassword(host, port, user, pass); } else { - picked = host + ":" + port; + picked = `${host}:${port}`; } } else { - notify(`Connection string "${picked}" is not valid.`, MessageKind.ERROR, { + notify(`Connection string (${picked}) is not valid.`, MessageKind.ERROR, { logger, }); return undefined;