From 76f8aad8fb158dc212dfee73348652f178663aec Mon Sep 17 00:00:00 2001 From: Richie Varghese Date: Sun, 14 Dec 2025 02:21:06 +0530 Subject: [PATCH 1/8] Bump version to 0.6.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bafcd78..8460b75 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "postgres-explorer", "displayName": "PostgreSQL Explorer", - "version": "0.6.0", + "version": "0.6.1", "description": "PostgreSQL database explorer for VS Code with notebook support", "publisher": "ric-v", "private": false, From 76513210fca0bd9b705eedb6f406377408be09df Mon Sep 17 00:00:00 2001 From: Richie Varghese Date: Sun, 14 Dec 2025 02:24:59 +0530 Subject: [PATCH 2/8] Bump version to 0.6.2 --- package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 8460b75..8d8a044 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,19 @@ { - "name": "postgres-explorer", - "displayName": "PostgreSQL Explorer", - "version": "0.6.1", - "description": "PostgreSQL database explorer for VS Code with notebook support", + "name": "pgstudio", + "displayName": "PgStudio (previously PostgreSQL Explorer)", + "version": "0.6.2", + "description": "PgStudio is a PostgreSQL database explorer for VS Code with notebook support", "publisher": "ric-v", "private": false, "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/dev-asterix/yape.git" + "url": "https://github.com/dev-asterix/PgStudio.git" }, "bugs": { - "url": "https://github.com/dev-asterix/yape/issues" + "url": "https://github.com/dev-asterix/PgStudio/issues" }, - "homepage": "https://github.com/dev-asterix/yape#readme", + "homepage": "https://pgstudio.astrx.dev/", "icon": "resources/postgres-explorer.png", "galleryBanner": { "color": "#C80000", From 888f60da502bb24497ebccbf23cb176c44988379 Mon Sep 17 00:00:00 2001 From: Richie Varghese Date: Sun, 14 Dec 2025 02:26:18 +0530 Subject: [PATCH 3/8] Bump version to 0.6.3 --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 8d8a044..a6888ee 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "pgstudio", + "name": "postgres-explorer", "displayName": "PgStudio (previously PostgreSQL Explorer)", - "version": "0.6.2", - "description": "PgStudio is a PostgreSQL database explorer for VS Code with notebook support", + "version": "0.6.3", + "description": "PostgreSQL database explorer for VS Code with notebook support", "publisher": "ric-v", "private": false, "license": "MIT", From 019ec9df493c8131fbe0a06c6524a5c5e9e1f8c8 Mon Sep 17 00:00:00 2001 From: Richie Varghese Date: Sun, 14 Dec 2025 10:23:24 +0530 Subject: [PATCH 4/8] Bump version to 0.6.5 --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index a6888ee..d1ccf98 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "postgres-explorer", - "displayName": "PgStudio (previously PostgreSQL Explorer)", - "version": "0.6.3", + "name": "pgstudio", + "displayName": "[DEPRECATED] PgStudio (previously PostgreSQL Explorer)", + "version": "0.6.5", "description": "PostgreSQL database explorer for VS Code with notebook support", "publisher": "ric-v", "private": false, From c97197992a0c2c23cd1f9bec12583299bf75610a Mon Sep 17 00:00:00 2001 From: Richie Varghese Date: Sun, 14 Dec 2025 10:25:13 +0530 Subject: [PATCH 5/8] Bump version to 0.6.6 --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d1ccf98..1d11288 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "pgstudio", - "displayName": "[DEPRECATED] PgStudio (previously PostgreSQL Explorer)", - "version": "0.6.5", + "name": "postgres-explorer", + "displayName": "PgStudio (PostgreSQL Explorer)", + "version": "0.6.6", "description": "PostgreSQL database explorer for VS Code with notebook support", "publisher": "ric-v", "private": false, From 746ff1e7a9c8aa33d6e4bafe08fae668e3b0684d Mon Sep 17 00:00:00 2001 From: Richie Varghese Date: Sun, 14 Dec 2025 10:38:05 +0530 Subject: [PATCH 6/8] feat: Add Foreign Data Wrappers (FDWs) to feature lists and documentation. --- MARKETPLACE.md | 6 ++++-- README.md | 3 ++- docs/index.html | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/MARKETPLACE.md b/MARKETPLACE.md index 1890201..875e2b6 100644 --- a/MARKETPLACE.md +++ b/MARKETPLACE.md @@ -35,8 +35,9 @@ | 🔌 **Secure Connections** | Manage multiple connections with VS Code SecretStorage encryption | | 📊 **Live Dashboard** | Real-time metrics, active query monitoring, and performance graphs | | 📓 **SQL Notebooks** | Interactive notebooks with rich output, AI assistance, and export options | -| 🌳 **Database Explorer** | Browse tables, views, functions, types, extensions, and roles | +| 🌳 **Database Explorer** | Browse tables, views, functions, types, extensions, roles, and FDWs | | 🛠️ **Object Operations** | Full CRUD operations, scripts, VACUUM, ANALYZE, REINDEX | +| 🌍 **Foreign Data Wrappers** | Manage foreign servers, user mappings, and tables | | 🤖 **AI-Powered** | GitHub Copilot, OpenAI, Anthropic, and Google Gemini integration | | ⌨️ **Developer Tools** | IntelliSense, keyboard shortcuts, PSQL terminal access | | 📤 **Export Data** | Export query results to CSV, JSON, or Excel formats | @@ -103,7 +104,7 @@ Navigate your database with an intuitive hierarchical tree view: ├── 🔄 Materialized Views ├── ⚙️ Functions ├── 🏷️ Types - ├── 🔗 Foreign Tables + ├── 🌍 Foreign Data Wrappers ├── 🧩 Extensions └── 👥 Roles ``` @@ -176,6 +177,7 @@ code --install-extension ric-v.postgres-explorer | 🔄 **Materialized Views** | Refresh, View Data, Edit, Drop | | ⚙️ **Functions** | View, Edit, Call with Parameters, Drop | | 🏷️ **Types** | View Properties, Edit, Drop | +| 🌍 **Foreign Data Wrappers** | Create/Drop Server, User Mappings, Import Schema | | 🔗 **Foreign Tables** | View, Edit, Drop | | 🧩 **Extensions** | Enable, Disable, Drop | | 👥 **Roles** | Grant/Revoke Permissions, Edit, Drop | diff --git a/README.md b/README.md index 22a5aa7..280b103 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,9 @@ - 🔌 **Secure Connections** — VS Code SecretStorage encryption - 📊 **Live Dashboard** — Real-time metrics & query monitoring - 📓 **SQL Notebooks** — Interactive notebooks with AI assistance -- 🌳 **Database Explorer** — Browse tables, views, functions, types +- 🌳 **Database Explorer** — Browse tables, views, functions, types, FDWs - 🛠️ **Object Operations** — CRUD, scripts, VACUUM, ANALYZE, REINDEX +- 🌍 **Foreign Data Wrappers** — Manage foreign servers, user mappings & tables - 🤖 **AI-Powered** — GitHub Copilot, OpenAI, Anthropic, Gemini - 📤 **Export Data** — Export results to CSV, JSON, or Excel diff --git a/docs/index.html b/docs/index.html index d9e14d9..02b3641 100644 --- a/docs/index.html +++ b/docs/index.html @@ -226,7 +226,7 @@

Database Explorer

  • Tables, Views, Functions
  • Materialized Views
  • -
  • Types & Extensions
  • +
  • Types, Extensions, FDWs
  • Roles & Permissions
From 05597039c47ffd77eaab7831b5e6f55f287443ff Mon Sep 17 00:00:00 2001 From: Richie Varghese Date: Sun, 14 Dec 2025 12:14:16 +0530 Subject: [PATCH 7/8] Bump version to 0.6.7 --- .github/workflows/publish.yml | 2 + package-lock.json | 107 ++++++++++++++- package.json | 8 +- src/common/types.ts | 7 + src/connectionForm.ts | 219 +++++++++++++++++++++++++++--- src/connectionManagement.ts | 14 +- src/dashboard/DashboardHtml.ts | 26 ++++ src/extension.ts | 4 +- src/renderer_v2.ts | 6 +- src/services/ConnectionManager.ts | 33 +++-- src/services/SSHService.ts | 79 +++++++++++ tsconfig.json | 1 + 12 files changed, 462 insertions(+), 44 deletions(-) create mode 100644 src/services/SSHService.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 63302b4..31087f3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,6 @@ name: Publish Extension +permissions: + contents: read on: push: diff --git a/package-lock.json b/package-lock.json index 575141f..119e54b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "postgres-explorer", - "version": "0.5.3", + "version": "0.6.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "postgres-explorer", - "version": "0.5.3", + "version": "0.6.6", "license": "MIT", "dependencies": { "chart.js": "^4.5.1", "esbuild": ">=0.25.0", - "pg": "^8.11.3" + "pg": "^8.11.3", + "ssh2": "^1.15.0" }, "devDependencies": { "@types/chai": "^5.2.3", @@ -20,6 +21,7 @@ "@types/node": "^16.18.126", "@types/pg": "^8.11.11", "@types/sinon": "^21.0.0", + "@types/ssh2": "^1.15.0", "@types/vscode": "^1.80.0", "@types/vscode-notebook-renderer": "^1.72.4", "chai": "^6.2.1", @@ -1977,6 +1979,26 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ssh2": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@types/vscode": { "version": "1.106.1", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.106.1.tgz", @@ -2357,6 +2379,15 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2434,6 +2465,15 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/binaryextensions": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", @@ -2585,6 +2625,15 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -2933,6 +2982,20 @@ "dev": true, "license": "MIT" }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -5205,6 +5268,13 @@ "dev": true, "license": "ISC" }, + "node_modules/nan": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz", + "integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==", + "license": "MIT", + "optional": true + }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", @@ -6497,7 +6567,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, "license": "MIT" }, "node_modules/sax": { @@ -6880,6 +6949,23 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -7374,6 +7460,12 @@ "node": "*" } }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -7457,6 +7549,13 @@ "node": ">=20.18.1" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, "node_modules/unicorn-magic": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", diff --git a/package.json b/package.json index 1d11288..5f20292 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "postgres-explorer", "displayName": "PgStudio (PostgreSQL Explorer)", - "version": "0.6.6", + "version": "0.6.7", "description": "PostgreSQL database explorer for VS Code with notebook support", "publisher": "ric-v", "private": false, @@ -1593,7 +1593,7 @@ "main": "./dist/extension.js", "scripts": { "vscode:prepublish": "npm run esbuild-base --minify && npm run esbuild-renderer --minify", - "esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node", + "esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --external:ssh2 --format=cjs --platform=node", "esbuild-renderer": "esbuild ./src/renderer_v2.ts --bundle --outfile=dist/renderer_v2.js --format=esm --platform=browser", "compile": "tsc -p ./ && npm run esbuild-renderer", "watch": "tsc -watch -p ./", @@ -1605,7 +1605,8 @@ "dependencies": { "chart.js": "^4.5.1", "esbuild": ">=0.25.0", - "pg": "^8.11.3" + "pg": "^8.11.3", + "ssh2": "^1.15.0" }, "devDependencies": { "@types/chai": "^5.2.3", @@ -1613,6 +1614,7 @@ "@types/module-alias": "^2.0.4", "@types/node": "^16.18.126", "@types/pg": "^8.11.11", + "@types/ssh2": "^1.15.0", "@types/sinon": "^21.0.0", "@types/vscode": "^1.80.0", "@types/vscode-notebook-renderer": "^1.72.4", diff --git a/src/common/types.ts b/src/common/types.ts index c454816..59dd7af 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -6,6 +6,13 @@ export interface ConnectionConfig { username?: string; password?: string; database?: string; + ssh?: { + enabled: boolean; + host: string; + port: number; + username: string; + privateKeyPath?: string; + }; } export interface PostgresMetadata { diff --git a/src/connectionForm.ts b/src/connectionForm.ts index 71c88a5..20f6dea 100644 --- a/src/connectionForm.ts +++ b/src/connectionForm.ts @@ -1,5 +1,6 @@ import { Client } from 'pg'; import * as vscode from 'vscode'; +import { SSHService } from './services/SSHService'; export interface ConnectionInfo { id: string; @@ -9,6 +10,13 @@ export interface ConnectionInfo { username?: string; password?: string; database?: string; + ssh?: { + enabled: boolean; + host: string; + port: number; + username: string; + privateKeyPath?: string; + }; } export class ConnectionFormPanel { @@ -17,7 +25,7 @@ export class ConnectionFormPanel { private readonly _extensionUri: vscode.Uri; private _disposables: vscode.Disposable[] = []; - private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri, private readonly _extensionContext: vscode.ExtensionContext) { + private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri, private readonly _extensionContext: vscode.ExtensionContext, private readonly _connectionToEdit?: ConnectionInfo) { this._panel = panel; this._extensionUri = extensionUri; @@ -29,14 +37,26 @@ export class ConnectionFormPanel { switch (message.command) { case 'testConnection': try { - // First try with specified database - const client = new Client({ - host: message.connection.host, - port: message.connection.port, + const config: any = { user: message.connection.username || undefined, password: message.connection.password || undefined, database: message.connection.database || 'postgres' - }); + }; + + if (message.connection.ssh && message.connection.ssh.enabled) { + const stream = await SSHService.getInstance().createStream( + message.connection.ssh, + message.connection.host, + message.connection.port + ); + config.stream = stream; + } else { + config.host = message.connection.host; + config.port = message.connection.port; + } + + // First try with specified database + const client = new Client(config); try { await client.connect(); const result = await client.query('SELECT version()'); @@ -46,6 +66,12 @@ export class ConnectionFormPanel { version: result.rows[0].version }); } catch (err: any) { + if (config.stream) { + // If using stream, we can't easily fallback without creating a new stream + // simpler to just throw for now or re-create stream + throw err; + } + // If database doesn't exist, try postgres database if (err.code === '3D000' && message.connection.database !== 'postgres') { const fallbackClient = new Client({ @@ -76,13 +102,25 @@ export class ConnectionFormPanel { case 'saveConnection': try { - const client = new Client({ - host: message.connection.host, - port: message.connection.port, + const config: any = { user: message.connection.username || undefined, password: message.connection.password || undefined, database: 'postgres' - }); + }; + + if (message.connection.ssh && message.connection.ssh.enabled) { + const stream = await SSHService.getInstance().createStream( + message.connection.ssh, + message.connection.host, + message.connection.port + ); + config.stream = stream; + } else { + config.host = message.connection.host; + config.port = message.connection.port; + } + + const client = new Client(config); await client.connect(); @@ -92,18 +130,30 @@ export class ConnectionFormPanel { const connections = this.getStoredConnections(); const newConnection: ConnectionInfo = { - id: Date.now().toString(), + id: this._connectionToEdit ? this._connectionToEdit.id : Date.now().toString(), name: message.connection.name, host: message.connection.host, port: message.connection.port, username: message.connection.username || undefined, password: message.connection.password || undefined, - database: message.connection.database // Add database to saved connection + database: message.connection.database, + ssh: message.connection.ssh }; - connections.push(newConnection); + + if (this._connectionToEdit) { + const index = connections.findIndex(c => c.id === this._connectionToEdit!.id); + if (index !== -1) { + connections[index] = newConnection; + } else { + connections.push(newConnection); + } + } else { + connections.push(newConnection); + } + await this.storeConnections(connections); - vscode.window.showInformationMessage('Connection saved successfully!'); + vscode.window.showInformationMessage(`Connection ${this._connectionToEdit ? 'updated' : 'saved'} successfully!`); vscode.commands.executeCommand('postgres-explorer.refreshConnections'); this._panel.dispose(); } catch (err: any) { @@ -118,7 +168,7 @@ export class ConnectionFormPanel { ); } - public static show(extensionUri: vscode.Uri, extensionContext: vscode.ExtensionContext) { + public static show(extensionUri: vscode.Uri, extensionContext: vscode.ExtensionContext, connectionToEdit?: ConnectionInfo) { if (ConnectionFormPanel.currentPanel) { ConnectionFormPanel.currentPanel._panel.reveal(); return; @@ -126,14 +176,14 @@ export class ConnectionFormPanel { const panel = vscode.window.createWebviewPanel( 'connectionForm', - 'Add PostgreSQL Connection', + connectionToEdit ? 'Edit Connection' : 'Add PostgreSQL Connection', vscode.ViewColumn.One, { enableScripts: true } ); - ConnectionFormPanel.currentPanel = new ConnectionFormPanel(panel, extensionUri, extensionContext); + ConnectionFormPanel.currentPanel = new ConnectionFormPanel(panel, extensionUri, extensionContext, connectionToEdit); } private async _initialize() { @@ -145,15 +195,25 @@ export class ConnectionFormPanel { this._panel.webview.html = await this._getHtmlForWebview(this._panel.webview); } - private _getHtmlForWebview(webview: vscode.Webview): string { + private async _getHtmlForWebview(webview: vscode.Webview): Promise { const logoPath = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'resources', 'postgres-vsc-icon.png')); + let connectionData = null; + if (this._connectionToEdit) { + // Get the password from secret storage + const password = await this._extensionContext.secrets.get(`postgres-password-${this._connectionToEdit.id}`); + connectionData = { + ...this._connectionToEdit, + password + }; + } + return ` - Add PostgreSQL Connection + ${this._connectionToEdit ? 'Edit Connection' : 'Add PostgreSQL Connection'}
- PostgreSQL + Logo

Connection Management

Manage your PostgreSQL database connections

-
- -
+
@@ -676,6 +456,10 @@ export class ConnectionManagementPanel { function addConnection() { vscode.postMessage({ command: 'addConnection' }); } + + function editConnection(id) { + vscode.postMessage({ command: 'edit', id: id }); + } function refreshConnections() { vscode.postMessage({ command: 'refresh' }); @@ -683,92 +467,83 @@ export class ConnectionManagementPanel { function testConnection(id) { const btn = document.querySelector(\`[data-test-id="\${id}"]\`); - const result = document.getElementById(\`test-result-\${id}\`); - - btn.classList.add('loading'); + const originalText = btn.innerHTML; btn.textContent = 'Testing...'; - result.style.display = 'none'; + btn.disabled = true; - vscode.postMessage({ - command: 'test', - id: id - }); - } - - // Add event delegation for delete buttons - document.addEventListener('click', function(event) { - const deleteBtn = event.target.closest('.btn-delete'); - if (deleteBtn) { - const id = deleteBtn.getAttribute('data-connection-id'); - const name = deleteBtn.getAttribute('data-connection-name'); - - if (id) { - // Show custom confirmation - const card = deleteBtn.closest('.connection-card'); - const existingConfirm = card.querySelector('.delete-confirmation'); - - if (existingConfirm) { - existingConfirm.remove(); - return; - } - - const confirmDiv = document.createElement('div'); - confirmDiv.className = 'delete-confirmation'; - confirmDiv.innerHTML = \` -
-

Delete "\${name}"?

-
- - -
-
- \`; - - card.querySelector('.card-actions').appendChild(confirmDiv); - - confirmDiv.querySelector('.btn-confirm-yes').addEventListener('click', function() { - vscode.postMessage({ - command: 'delete', - id: id - }); - }); - - confirmDiv.querySelector('.btn-confirm-no').addEventListener('click', function() { - confirmDiv.remove(); - }); - } - } - }); + vscode.postMessage({ command: 'test', id: id }); + + // Store original text + btn.setAttribute('data-original-text', originalText); + } + + function showDeleteConfirm(id) { + const card = document.querySelector(\`[data-card-id="\${id}"]\`); + if (card.querySelector('.delete-confirm-overlay')) return; + + const overlay = document.createElement('div'); + overlay.className = 'delete-confirm-overlay'; + overlay.innerHTML = \` +

Delete this connection?

+
+ + +
+ \`; + card.appendChild(overlay); + } + + function deleteConnection(id) { + vscode.postMessage({ command: 'delete', id: id }); + } window.addEventListener('message', event => { const message = event.data; if (message.type === 'testSuccess') { const btn = document.querySelector(\`[data-test-id="\${message.id}"]\`); - const result = document.getElementById(\`test-result-\${message.id}\`); + const card = document.querySelector(\`[data-card-id="\${message.id}"]\`); - btn.classList.remove('loading'); - btn.textContent = '✓ Test'; + btn.innerHTML = 'Scan ✓'; + btn.disabled = false; - result.className = 'test-result success'; - result.textContent = '✓ Connection successful!'; - result.style.display = 'block'; + showNotification(card, 'Connection successful!', 'success'); setTimeout(() => { - result.style.display = 'none'; - }, 5000); + const original = btn.getAttribute('data-original-text'); + if(original) btn.innerHTML = original; + }, 3000); + } else if (message.type === 'testError') { - const btn = document.querySelector(\`[data-test-id="\${message.id}"]\`); - const result = document.getElementById(\`test-result-\${message.id}\`); + const btn = document.querySelector(\`[data-test-id="\${message.id}"]\`); + const card = document.querySelector(\`[data-card-id="\${message.id}"]\`); - btn.classList.remove('loading'); - btn.textContent = '✗ Test'; + btn.innerHTML = 'Error ✕'; + btn.disabled = false; - result.className = 'test-result error'; - result.textContent = \`✗ \${message.error}\`; - result.style.display = 'block'; + showNotification(card, message.error, 'error'); + setTimeout(() => { + const original = btn.getAttribute('data-original-text'); + if(original) btn.innerHTML = original; + }, 3000); } }); + + function showNotification(card, text, type) { + const existing = card.querySelector('.test-result-overlay'); + if(existing) existing.remove(); + + const el = document.createElement('div'); + el.className = \`test-result-overlay \${type}\`; + el.textContent = text; + card.appendChild(el); + el.style.display = 'block'; + + setTimeout(() => { + el.style.opacity = '0'; + setTimeout(() => el.remove(), 300); + }, 4000); + } `; @@ -776,66 +551,53 @@ export class ConnectionManagementPanel { private _getConnectionCardHtml(conn: ConnectionInfo & { hasPassword: boolean }): string { const connectionString = this._buildConnectionString(conn); - const authBadge = conn.hasPassword || conn.username - ? '✓ Auth' - : 'No Auth'; + const authStatus = conn.hasPassword || conn.username + ? 'Auth ✓' + : 'No Auth'; + + // Escaping helper + const escape = (s: string | undefined) => this._escapeHtml(s || ''); return ` -
+
-
-
- 🗄️ - ${this._escapeHtml(conn.name)} -
+
+ 🗄️ + ${escape(conn.name)}
-
-
- - Live -
- ${authBadge} +
+ Live + ${authStatus}
- 🌐 Host: - ${this._escapeHtml(conn.host)}:${conn.port} + ${escape(conn.host)}:${conn.port}
- ${conn.database ? `
- 💾 Database: - ${this._escapeHtml(conn.database)} + ${escape(conn.database)}
- ` : ''} - ${conn.username ? `
- 👤 User: - ${this._escapeHtml(conn.username)} + ${escape(conn.username)}
- ` : ''}
-
- ${this._escapeHtml(connectionString)} -
- -
-
- + -
-
- `; +
`; } private _buildConnectionString(conn: ConnectionInfo): string { diff --git a/src/providers/DatabaseTreeProvider.ts b/src/providers/DatabaseTreeProvider.ts index 4d149fc..5dc794b 100644 --- a/src/providers/DatabaseTreeProvider.ts +++ b/src/providers/DatabaseTreeProvider.ts @@ -66,10 +66,10 @@ export class DatabaseTreeProvider implements vscode.TreeDataProvider c.id === element.connectionId); @@ -97,11 +97,6 @@ export class DatabaseTreeProvider implements vscode.TreeDataProvider