@hienlh/ppm 0.13.48 → 0.13.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/{audio-preview-BFc4v5Tx.js → audio-preview-CQ-l5P8I.js} +2 -2
- package/dist/web/assets/{audio-preview-BFc4v5Tx.js.map → audio-preview-CQ-l5P8I.js.map} +1 -1
- package/dist/web/assets/{chat-tab-x1rBGHj5.js → chat-tab-DbuBr2ax.js} +4 -4
- package/dist/web/assets/{chat-tab-x1rBGHj5.js.map → chat-tab-DbuBr2ax.js.map} +1 -1
- package/dist/web/assets/{code-editor-BgyAZcQX.js → code-editor-DEa0t62y.js} +3 -3
- package/dist/web/assets/{code-editor-BgyAZcQX.js.map → code-editor-DEa0t62y.js.map} +1 -1
- package/dist/web/assets/{conflict-editor-Dhc1lem7.js → conflict-editor-D5H9urYy.js} +2 -2
- package/dist/web/assets/{conflict-editor-Dhc1lem7.js.map → conflict-editor-D5H9urYy.js.map} +1 -1
- package/dist/web/assets/{database-viewer-DnnjO38W.js → database-viewer-CW60ytCl.js} +2 -2
- package/dist/web/assets/{database-viewer-DnnjO38W.js.map → database-viewer-CW60ytCl.js.map} +1 -1
- package/dist/web/assets/{diff-viewer-B6q9wXD6.js → diff-viewer-BfatMgWw.js} +2 -2
- package/dist/web/assets/{diff-viewer-B6q9wXD6.js.map → diff-viewer-BfatMgWw.js.map} +1 -1
- package/dist/web/assets/{extension-webview-CMBEb4FF.js → extension-webview-DKSDoW_g.js} +2 -2
- package/dist/web/assets/{extension-webview-CMBEb4FF.js.map → extension-webview-DKSDoW_g.js.map} +1 -1
- package/dist/web/assets/{glide-data-grid-BLcBAxgp.js → glide-data-grid-Bx48618B.js} +2 -2
- package/dist/web/assets/{glide-data-grid-BLcBAxgp.js.map → glide-data-grid-Bx48618B.js.map} +1 -1
- package/dist/web/assets/{image-preview-YpWk7AOb.js → image-preview-ClY2xl1B.js} +2 -2
- package/dist/web/assets/{image-preview-YpWk7AOb.js.map → image-preview-ClY2xl1B.js.map} +1 -1
- package/dist/web/assets/{index-hxAGD2rx.js → index-DkQ6jVSH.js} +5 -5
- package/dist/web/assets/{index-hxAGD2rx.js.map → index-DkQ6jVSH.js.map} +1 -1
- package/dist/web/assets/keybindings-store-DrAeg6Gw.js +1 -0
- package/dist/web/assets/{markdown-renderer-BAAmsTL9.js → markdown-renderer-BmMmo0F-.js} +2 -2
- package/dist/web/assets/{markdown-renderer-BAAmsTL9.js.map → markdown-renderer-BmMmo0F-.js.map} +1 -1
- package/dist/web/assets/notification-store-Bukl8bKo.js +1 -0
- package/dist/web/assets/{pdf-preview-DWe7ARXI.js → pdf-preview-YylEP_Su.js} +2 -2
- package/dist/web/assets/{pdf-preview-DWe7ARXI.js.map → pdf-preview-YylEP_Su.js.map} +1 -1
- package/dist/web/assets/{port-forwarding-tab-R5V7zkKO.js → port-forwarding-tab-COdo70kU.js} +2 -2
- package/dist/web/assets/{port-forwarding-tab-R5V7zkKO.js.map → port-forwarding-tab-COdo70kU.js.map} +1 -1
- package/dist/web/assets/{postgres-viewer-AxCUyDQP.js → postgres-viewer-3y9VshZZ.js} +2 -2
- package/dist/web/assets/{postgres-viewer-AxCUyDQP.js.map → postgres-viewer-3y9VshZZ.js.map} +1 -1
- package/dist/web/assets/{settings-tab-CJ6w6q2s.js → settings-tab-sYavdJk-.js} +1 -1
- package/dist/web/assets/{sql-query-editor-DhZvNbKv.js → sql-query-editor-DlBYx1Ye.js} +2 -2
- package/dist/web/assets/{sql-query-editor-DhZvNbKv.js.map → sql-query-editor-DlBYx1Ye.js.map} +1 -1
- package/dist/web/assets/{sqlite-viewer-Qv8TlhPb.js → sqlite-viewer-Bj8oPYho.js} +2 -2
- package/dist/web/assets/{sqlite-viewer-Qv8TlhPb.js.map → sqlite-viewer-Bj8oPYho.js.map} +1 -1
- package/dist/web/assets/{terminal-tab-9hLQtUUT.js → terminal-tab-B7ECmf95.js} +2 -2
- package/dist/web/assets/{terminal-tab-9hLQtUUT.js.map → terminal-tab-B7ECmf95.js.map} +1 -1
- package/dist/web/assets/{video-preview-C-tj7tok.js → video-preview-CT78iZBo.js} +2 -2
- package/dist/web/assets/{video-preview-C-tj7tok.js.map → video-preview-CT78iZBo.js.map} +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/services/db.service.ts +28 -0
- package/src/types/extension.ts +1 -1
- package/src/web/components/settings/extension-manager-section.tsx +2 -2
- package/dist/web/assets/keybindings-store-Dn9ANcCK.js +0 -1
- package/dist/web/assets/notification-store-DyVQiPv9.js +0 -1
- package/packages/ext-database/package.json +0 -48
- package/packages/ext-database/src/connection-tree.ts +0 -201
- package/packages/ext-database/src/extension.ts +0 -578
- package/packages/ext-database/src/query-panel.ts +0 -133
- package/packages/ext-database/src/table-viewer-panel.ts +0 -420
- package/packages/ext-database/tsconfig.json +0 -8
|
@@ -1,578 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @ppm/ext-database — Database Viewer extension for PPM.
|
|
3
|
-
* Provides a sidebar tree view of DB connections/tables/columns
|
|
4
|
-
* and a full-featured table viewer webview with inline editing,
|
|
5
|
-
* pagination, and SQL query panel.
|
|
6
|
-
*/
|
|
7
|
-
import type { ExtensionContext } from "@ppm/vscode-compat";
|
|
8
|
-
import { ConnectionTreeProvider } from "./connection-tree.ts";
|
|
9
|
-
import { getQueryPanelHtml } from "./query-panel.ts";
|
|
10
|
-
import { getTableViewerHtml } from "./table-viewer-panel.ts";
|
|
11
|
-
|
|
12
|
-
/** PPM vscode-compat API namespace (passed as second arg by Worker) */
|
|
13
|
-
interface VscodeApi {
|
|
14
|
-
commands: {
|
|
15
|
-
registerCommand(command: string, callback: (...args: unknown[]) => unknown): { dispose(): void };
|
|
16
|
-
};
|
|
17
|
-
window: {
|
|
18
|
-
showInformationMessage(message: string, ...items: string[]): Promise<string | undefined>;
|
|
19
|
-
showErrorMessage(message: string, ...items: string[]): Promise<string | undefined>;
|
|
20
|
-
showQuickPick(items: string[], options?: { placeHolder?: string }): Promise<string | undefined>;
|
|
21
|
-
showInputBox(options?: { prompt?: string; placeHolder?: string; value?: string }): Promise<string | undefined>;
|
|
22
|
-
createTreeView(viewId: string, options: { treeDataProvider: unknown }): { dispose(): void };
|
|
23
|
-
createWebviewPanel(viewType: string, title: string, showOptions: unknown): {
|
|
24
|
-
webview: {
|
|
25
|
-
html: string;
|
|
26
|
-
onDidReceiveMessage: (listener: (msg: unknown) => void) => { dispose(): void };
|
|
27
|
-
postMessage(message: unknown): Promise<boolean>;
|
|
28
|
-
};
|
|
29
|
-
onDidDispose: (listener: () => void) => { dispose(): void };
|
|
30
|
-
dispose(): void;
|
|
31
|
-
};
|
|
32
|
-
createStatusBarItem(alignment?: unknown, priority?: number): {
|
|
33
|
-
text: string; tooltip?: string; command?: string;
|
|
34
|
-
show(): void; hide(): void; dispose(): void;
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
|
-
EventEmitter: new <T>() => { fire(data?: T): void; event: unknown; dispose(): void };
|
|
38
|
-
StatusBarAlignment: { Left: number; Right: number };
|
|
39
|
-
ViewColumn: { Active: number };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Server base URL for fetch() calls (set by Worker before activation)
|
|
43
|
-
let baseUrl = "";
|
|
44
|
-
|
|
45
|
-
export function activate(context: ExtensionContext, vscode: VscodeApi): void {
|
|
46
|
-
baseUrl = (globalThis as any).__PPM_BASE_URL__ || "";
|
|
47
|
-
|
|
48
|
-
// --- Tree View ---
|
|
49
|
-
const emitter = new vscode.EventEmitter<unknown>();
|
|
50
|
-
const treeProvider = new ConnectionTreeProvider(
|
|
51
|
-
{ fire: (el?: unknown) => emitter.fire(el), event: emitter.event },
|
|
52
|
-
baseUrl,
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const treeView = vscode.window.createTreeView("ppm-db.connections", {
|
|
56
|
-
treeDataProvider: treeProvider,
|
|
57
|
-
});
|
|
58
|
-
context.subscriptions.push(treeView);
|
|
59
|
-
|
|
60
|
-
// --- Commands ---
|
|
61
|
-
context.subscriptions.push(
|
|
62
|
-
vscode.commands.registerCommand("ppm-db.openViewer", (...args: unknown[]) => {
|
|
63
|
-
const connectionId = (args[0] as number) ?? 1;
|
|
64
|
-
const connectionName = (args[1] as string) ?? "Database";
|
|
65
|
-
const tableName = args[2] as string | undefined;
|
|
66
|
-
const schemaName = (args[3] as string) ?? "public";
|
|
67
|
-
if (tableName) {
|
|
68
|
-
openTableViewer(vscode, context, connectionId, connectionName, tableName, schemaName);
|
|
69
|
-
} else {
|
|
70
|
-
openQueryPanel(vscode, context, connectionId, connectionName);
|
|
71
|
-
}
|
|
72
|
-
}),
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
context.subscriptions.push(
|
|
76
|
-
vscode.commands.registerCommand("ppm-db.runQuery", () => {
|
|
77
|
-
openQueryPanel(vscode, context, 1, "Default");
|
|
78
|
-
}),
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
context.subscriptions.push(
|
|
82
|
-
vscode.commands.registerCommand("ppm-db.refreshConnections", () => {
|
|
83
|
-
treeProvider.refresh();
|
|
84
|
-
}),
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
context.subscriptions.push(
|
|
88
|
-
vscode.commands.registerCommand("ppm-db.refreshConnection", () => {
|
|
89
|
-
treeProvider.refresh();
|
|
90
|
-
}),
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
context.subscriptions.push(
|
|
94
|
-
vscode.commands.registerCommand("ppm-db.addConnection", async () => {
|
|
95
|
-
await addConnection(vscode, treeProvider);
|
|
96
|
-
}),
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
context.subscriptions.push(
|
|
100
|
-
vscode.commands.registerCommand("ppm-db.editConnection", async (...args: unknown[]) => {
|
|
101
|
-
const connectionId = args[0] as number;
|
|
102
|
-
if (connectionId) await editConnection(vscode, treeProvider, connectionId);
|
|
103
|
-
}),
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
context.subscriptions.push(
|
|
107
|
-
vscode.commands.registerCommand("ppm-db.deleteConnection", async (...args: unknown[]) => {
|
|
108
|
-
const connectionId = args[0] as number;
|
|
109
|
-
const connectionName = (args[1] as string) ?? "this connection";
|
|
110
|
-
if (!connectionId) return;
|
|
111
|
-
const confirm = await vscode.window.showQuickPick(["Yes, delete", "Cancel"], {
|
|
112
|
-
placeHolder: `Delete "${connectionName}"?`,
|
|
113
|
-
});
|
|
114
|
-
if (confirm !== "Yes, delete") return;
|
|
115
|
-
try {
|
|
116
|
-
const res = await fetch(`${baseUrl}/api/db/connections/${connectionId}`, { method: "DELETE" });
|
|
117
|
-
const json = await res.json() as { ok: boolean; error?: string };
|
|
118
|
-
if (json.ok) {
|
|
119
|
-
await vscode.window.showInformationMessage(`Connection "${connectionName}" deleted`);
|
|
120
|
-
treeProvider.refresh();
|
|
121
|
-
} else {
|
|
122
|
-
await vscode.window.showErrorMessage(json.error ?? "Failed to delete connection");
|
|
123
|
-
}
|
|
124
|
-
} catch (e) {
|
|
125
|
-
await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
126
|
-
}
|
|
127
|
-
}),
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
context.subscriptions.push(
|
|
131
|
-
vscode.commands.registerCommand("ppm-db.testConnection", async (...args: unknown[]) => {
|
|
132
|
-
const connectionId = args[0] as number;
|
|
133
|
-
if (!connectionId) return;
|
|
134
|
-
try {
|
|
135
|
-
const res = await fetch(`${baseUrl}/api/db/connections/${connectionId}/test`, { method: "POST" });
|
|
136
|
-
const json = await res.json() as { ok: boolean; data?: { ok: boolean; error?: string } };
|
|
137
|
-
if (json.ok && json.data?.ok) {
|
|
138
|
-
await vscode.window.showInformationMessage("Connection successful");
|
|
139
|
-
} else {
|
|
140
|
-
await vscode.window.showErrorMessage(`Connection failed: ${json.data?.error ?? "Unknown error"}`);
|
|
141
|
-
}
|
|
142
|
-
} catch (e) {
|
|
143
|
-
await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
144
|
-
}
|
|
145
|
-
}),
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
context.subscriptions.push(
|
|
149
|
-
vscode.commands.registerCommand("ppm-db.exportConnections", async () => {
|
|
150
|
-
await exportConnections(vscode);
|
|
151
|
-
}),
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
context.subscriptions.push(
|
|
155
|
-
vscode.commands.registerCommand("ppm-db.importConnections", async () => {
|
|
156
|
-
await importConnections(vscode, treeProvider);
|
|
157
|
-
}),
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
// --- Status Bar ---
|
|
161
|
-
const statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 10);
|
|
162
|
-
statusItem.text = "DB";
|
|
163
|
-
statusItem.tooltip = "Database Viewer";
|
|
164
|
-
statusItem.command = "ppm-db.runQuery";
|
|
165
|
-
statusItem.show();
|
|
166
|
-
context.subscriptions.push(statusItem);
|
|
167
|
-
|
|
168
|
-
console.log("[ext-database] activated");
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export function deactivate(): void {
|
|
172
|
-
console.log("[ext-database] deactivated");
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// ---------------------------------------------------------------------------
|
|
176
|
-
// Table Viewer — full-featured data grid with inline editing & SQL panel
|
|
177
|
-
// ---------------------------------------------------------------------------
|
|
178
|
-
|
|
179
|
-
function openTableViewer(
|
|
180
|
-
vscode: VscodeApi,
|
|
181
|
-
context: ExtensionContext,
|
|
182
|
-
connectionId: number,
|
|
183
|
-
connectionName: string,
|
|
184
|
-
tableName: string,
|
|
185
|
-
schemaName: string,
|
|
186
|
-
): void {
|
|
187
|
-
const panel = vscode.window.createWebviewPanel(
|
|
188
|
-
"ppm-db.tableViewer",
|
|
189
|
-
`${connectionName} · ${tableName}`,
|
|
190
|
-
vscode.ViewColumn.Active,
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
panel.webview.html = getTableViewerHtml({ connectionId, connectionName, tableName, schemaName });
|
|
194
|
-
|
|
195
|
-
const msgDisposable = panel.webview.onDidReceiveMessage(async (raw: unknown) => {
|
|
196
|
-
const msg = raw as Record<string, unknown>;
|
|
197
|
-
const connId = (msg.connectionId as number) ?? connectionId;
|
|
198
|
-
const tbl = (msg.tableName as string) ?? tableName;
|
|
199
|
-
const schema = (msg.schemaName as string) ?? schemaName;
|
|
200
|
-
|
|
201
|
-
switch (msg.type) {
|
|
202
|
-
case "init":
|
|
203
|
-
case "refresh":
|
|
204
|
-
await sendTableData(panel, connId, tbl, schema, (msg.page as number) ?? 1);
|
|
205
|
-
break;
|
|
206
|
-
|
|
207
|
-
case "fetchPage":
|
|
208
|
-
await sendTableData(panel, connId, tbl, schema, (msg.page as number) ?? 1);
|
|
209
|
-
break;
|
|
210
|
-
|
|
211
|
-
case "executeQuery":
|
|
212
|
-
await handleQuery(panel, connId, msg.sql as string);
|
|
213
|
-
break;
|
|
214
|
-
|
|
215
|
-
case "updateCell":
|
|
216
|
-
await handleCellUpdate(panel, connId, tbl, schema, msg);
|
|
217
|
-
break;
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
context.subscriptions.push(msgDisposable);
|
|
221
|
-
panel.onDidDispose(() => msgDisposable.dispose());
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/** Fetch table data + schema and send to webview */
|
|
225
|
-
async function sendTableData(
|
|
226
|
-
panel: ReturnType<VscodeApi["window"]["createWebviewPanel"]>,
|
|
227
|
-
connectionId: number,
|
|
228
|
-
tableName: string,
|
|
229
|
-
schemaName: string,
|
|
230
|
-
page: number,
|
|
231
|
-
): Promise<void> {
|
|
232
|
-
try {
|
|
233
|
-
const [dataRes, schemaRes] = await Promise.all([
|
|
234
|
-
fetch(`${baseUrl}/api/db/connections/${connectionId}/data?table=${encodeURIComponent(tableName)}&schema=${schemaName}&page=${page}&limit=100`),
|
|
235
|
-
fetch(`${baseUrl}/api/db/connections/${connectionId}/schema?table=${encodeURIComponent(tableName)}&schema=${schemaName}`),
|
|
236
|
-
]);
|
|
237
|
-
const dataJson = await dataRes.json() as { ok: boolean; data?: { columns: string[]; rows: Record<string, unknown>[]; total: number; page: number; limit: number } };
|
|
238
|
-
const schemaJson = await schemaRes.json() as { ok: boolean; data?: { name: string; type: string; nullable: boolean; pk: boolean; defaultValue: string | null }[] };
|
|
239
|
-
|
|
240
|
-
if (!dataJson.ok) {
|
|
241
|
-
await panel.webview.postMessage({ type: "error", message: "Failed to load table data" });
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
await panel.webview.postMessage({
|
|
246
|
-
type: "tableData",
|
|
247
|
-
columns: dataJson.data?.columns ?? [],
|
|
248
|
-
rows: dataJson.data?.rows ?? [],
|
|
249
|
-
total: dataJson.data?.total ?? 0,
|
|
250
|
-
page: dataJson.data?.page ?? page,
|
|
251
|
-
limit: dataJson.data?.limit ?? 100,
|
|
252
|
-
schema: schemaJson.data ?? [],
|
|
253
|
-
});
|
|
254
|
-
} catch (e) {
|
|
255
|
-
const errMsg = e instanceof Error ? e.message : String(e);
|
|
256
|
-
await panel.webview.postMessage({ type: "error", message: errMsg });
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/** Execute SQL query and send result to webview */
|
|
261
|
-
async function handleQuery(
|
|
262
|
-
panel: ReturnType<VscodeApi["window"]["createWebviewPanel"]>,
|
|
263
|
-
connectionId: number,
|
|
264
|
-
sql: string,
|
|
265
|
-
): Promise<void> {
|
|
266
|
-
if (!sql) return;
|
|
267
|
-
try {
|
|
268
|
-
const start = Date.now();
|
|
269
|
-
const res = await fetch(`${baseUrl}/api/db/connections/${connectionId}/query`, {
|
|
270
|
-
method: "POST",
|
|
271
|
-
headers: { "Content-Type": "application/json" },
|
|
272
|
-
body: JSON.stringify({ sql }),
|
|
273
|
-
});
|
|
274
|
-
const json = await res.json() as { ok: boolean; data?: { columns: string[]; rows: Record<string, unknown>[]; rowsAffected: number; changeType: string } };
|
|
275
|
-
const duration = Date.now() - start;
|
|
276
|
-
|
|
277
|
-
if (!json.ok) {
|
|
278
|
-
await panel.webview.postMessage({ type: "queryError", error: (json as any).error ?? "Query failed" });
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
await panel.webview.postMessage({
|
|
283
|
-
type: "queryResult",
|
|
284
|
-
columns: json.data?.columns ?? [],
|
|
285
|
-
rows: json.data?.rows ?? [],
|
|
286
|
-
rowsAffected: json.data?.rowsAffected ?? 0,
|
|
287
|
-
changeType: json.data?.changeType ?? "select",
|
|
288
|
-
duration,
|
|
289
|
-
});
|
|
290
|
-
} catch (e) {
|
|
291
|
-
const errMsg = e instanceof Error ? e.message : String(e);
|
|
292
|
-
await panel.webview.postMessage({ type: "queryError", error: errMsg });
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/** Update a single cell value */
|
|
297
|
-
async function handleCellUpdate(
|
|
298
|
-
panel: ReturnType<VscodeApi["window"]["createWebviewPanel"]>,
|
|
299
|
-
connectionId: number,
|
|
300
|
-
tableName: string,
|
|
301
|
-
schemaName: string,
|
|
302
|
-
msg: Record<string, unknown>,
|
|
303
|
-
): Promise<void> {
|
|
304
|
-
try {
|
|
305
|
-
const res = await fetch(`${baseUrl}/api/db/connections/${connectionId}/cell`, {
|
|
306
|
-
method: "PUT",
|
|
307
|
-
headers: { "Content-Type": "application/json" },
|
|
308
|
-
body: JSON.stringify({
|
|
309
|
-
table: tableName,
|
|
310
|
-
schema: schemaName,
|
|
311
|
-
pkColumn: msg.pkColumn,
|
|
312
|
-
pkValue: msg.pkValue,
|
|
313
|
-
column: msg.column,
|
|
314
|
-
value: msg.value,
|
|
315
|
-
}),
|
|
316
|
-
});
|
|
317
|
-
const json = await res.json() as { ok: boolean; error?: string };
|
|
318
|
-
if (json.ok) {
|
|
319
|
-
await panel.webview.postMessage({ type: "cellUpdated" });
|
|
320
|
-
} else {
|
|
321
|
-
await panel.webview.postMessage({ type: "error", message: json.error ?? "Cell update failed" });
|
|
322
|
-
}
|
|
323
|
-
} catch (e) {
|
|
324
|
-
const errMsg = e instanceof Error ? e.message : String(e);
|
|
325
|
-
await panel.webview.postMessage({ type: "error", message: errMsg });
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// ---------------------------------------------------------------------------
|
|
330
|
-
// Query Panel — standalone SQL editor (opened from status bar)
|
|
331
|
-
// ---------------------------------------------------------------------------
|
|
332
|
-
|
|
333
|
-
function openQueryPanel(
|
|
334
|
-
vscode: VscodeApi,
|
|
335
|
-
context: ExtensionContext,
|
|
336
|
-
connectionId: number,
|
|
337
|
-
connectionName: string,
|
|
338
|
-
tableName?: string,
|
|
339
|
-
): void {
|
|
340
|
-
const panel = vscode.window.createWebviewPanel(
|
|
341
|
-
"ppm-db.queryPanel",
|
|
342
|
-
`Query: ${connectionName}`,
|
|
343
|
-
vscode.ViewColumn.Active,
|
|
344
|
-
);
|
|
345
|
-
|
|
346
|
-
panel.webview.html = getQueryPanelHtml(connectionName, tableName);
|
|
347
|
-
|
|
348
|
-
const msgDisposable = panel.webview.onDidReceiveMessage(async (raw: unknown) => {
|
|
349
|
-
const msg = raw as { type: string; sql?: string };
|
|
350
|
-
if (msg.type !== "executeQuery" || !msg.sql) return;
|
|
351
|
-
await handleQuery(panel, connectionId, msg.sql);
|
|
352
|
-
});
|
|
353
|
-
context.subscriptions.push(msgDisposable);
|
|
354
|
-
panel.onDidDispose(() => msgDisposable.dispose());
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// ---------------------------------------------------------------------------
|
|
358
|
-
// Add Connection — collect info via QuickPick + InputBox, then POST to API
|
|
359
|
-
// ---------------------------------------------------------------------------
|
|
360
|
-
|
|
361
|
-
/** Collect optional group/color/readonly via InputBox + QuickPick */
|
|
362
|
-
async function collectConnectionExtras(vscode: VscodeApi): Promise<{ groupName?: string; color?: string; readonly?: number } | null> {
|
|
363
|
-
const groupName = await vscode.window.showInputBox({ prompt: "Group name (optional)", placeHolder: "e.g. Production" });
|
|
364
|
-
if (groupName === undefined) return null; // cancelled
|
|
365
|
-
|
|
366
|
-
const COLOR_OPTIONS = ["None", "#ef4444", "#f97316", "#eab308", "#22c55e", "#06b6d4", "#3b82f6", "#8b5cf6", "#ec4899"];
|
|
367
|
-
const color = await vscode.window.showQuickPick(COLOR_OPTIONS, { placeHolder: "Pick a color (optional)" });
|
|
368
|
-
if (color === undefined) return null;
|
|
369
|
-
|
|
370
|
-
const readonlyChoice = await vscode.window.showQuickPick(["Yes (recommended)", "No"], {
|
|
371
|
-
placeHolder: "Readonly mode? (blocks write queries)",
|
|
372
|
-
});
|
|
373
|
-
if (readonlyChoice === undefined) return null;
|
|
374
|
-
|
|
375
|
-
return {
|
|
376
|
-
groupName: groupName || undefined,
|
|
377
|
-
color: color === "None" ? undefined : color,
|
|
378
|
-
readonly: readonlyChoice.startsWith("Yes") ? 1 : 0,
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
async function addConnection(
|
|
383
|
-
vscode: VscodeApi,
|
|
384
|
-
treeProvider: ConnectionTreeProvider,
|
|
385
|
-
): Promise<void> {
|
|
386
|
-
// 1. Pick type
|
|
387
|
-
const type = await vscode.window.showQuickPick(["postgres", "sqlite"], {
|
|
388
|
-
placeHolder: "Select database type",
|
|
389
|
-
}) as string | undefined;
|
|
390
|
-
if (!type) return;
|
|
391
|
-
|
|
392
|
-
// 2. Name
|
|
393
|
-
const name = await vscode.window.showInputBox({ prompt: "Connection name", placeHolder: "e.g. Production DB" });
|
|
394
|
-
if (!name) return;
|
|
395
|
-
|
|
396
|
-
// 3. Connection config
|
|
397
|
-
let connectionConfig: Record<string, string>;
|
|
398
|
-
if (type === "sqlite") {
|
|
399
|
-
const path = await vscode.window.showInputBox({ prompt: "SQLite file path", placeHolder: "/path/to/database.db" });
|
|
400
|
-
if (!path) return;
|
|
401
|
-
connectionConfig = { type: "sqlite", path };
|
|
402
|
-
} else {
|
|
403
|
-
const connStr = await vscode.window.showInputBox({
|
|
404
|
-
prompt: "PostgreSQL connection string",
|
|
405
|
-
placeHolder: "postgres://user:pass@host:5432/dbname",
|
|
406
|
-
});
|
|
407
|
-
if (!connStr) return;
|
|
408
|
-
connectionConfig = { type: "postgres", connectionString: connStr };
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// 4. Group, color, readonly
|
|
412
|
-
const extras = await collectConnectionExtras(vscode);
|
|
413
|
-
if (extras === null) return;
|
|
414
|
-
|
|
415
|
-
// 5. Create via API
|
|
416
|
-
try {
|
|
417
|
-
const res = await fetch(`${baseUrl}/api/db/connections`, {
|
|
418
|
-
method: "POST",
|
|
419
|
-
headers: { "Content-Type": "application/json" },
|
|
420
|
-
body: JSON.stringify({ type, name, connectionConfig, ...extras }),
|
|
421
|
-
});
|
|
422
|
-
const json = await res.json() as { ok: boolean; error?: string };
|
|
423
|
-
if (json.ok) {
|
|
424
|
-
await vscode.window.showInformationMessage(`Connection "${name}" created`);
|
|
425
|
-
treeProvider.refresh();
|
|
426
|
-
} else {
|
|
427
|
-
await vscode.window.showErrorMessage(json.error ?? "Failed to create connection");
|
|
428
|
-
}
|
|
429
|
-
} catch (e) {
|
|
430
|
-
await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// ---------------------------------------------------------------------------
|
|
435
|
-
// Edit Connection — update name, group, color, readonly via QuickPick/InputBox
|
|
436
|
-
// ---------------------------------------------------------------------------
|
|
437
|
-
|
|
438
|
-
async function editConnection(
|
|
439
|
-
vscode: VscodeApi,
|
|
440
|
-
treeProvider: ConnectionTreeProvider,
|
|
441
|
-
connectionId: number,
|
|
442
|
-
): Promise<void> {
|
|
443
|
-
// Fetch current connection data
|
|
444
|
-
let conn: { id: number; name: string; type: string; group_name?: string; color?: string; readonly?: number };
|
|
445
|
-
try {
|
|
446
|
-
const res = await fetch(`${baseUrl}/api/db/connections/${connectionId}`);
|
|
447
|
-
const json = await res.json() as { ok: boolean; data?: typeof conn };
|
|
448
|
-
if (!json.ok || !json.data) {
|
|
449
|
-
await vscode.window.showErrorMessage("Failed to load connection");
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
conn = json.data;
|
|
453
|
-
} catch (e) {
|
|
454
|
-
await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Name
|
|
459
|
-
const name = await vscode.window.showInputBox({ prompt: "Connection name", value: conn.name });
|
|
460
|
-
if (name === undefined) return;
|
|
461
|
-
|
|
462
|
-
// Connection config (optional update)
|
|
463
|
-
let connectionConfig: Record<string, string> | undefined;
|
|
464
|
-
const updateConfig = await vscode.window.showQuickPick(["Keep current", "Update connection config"], {
|
|
465
|
-
placeHolder: "Connection config",
|
|
466
|
-
});
|
|
467
|
-
if (updateConfig === undefined) return;
|
|
468
|
-
if (updateConfig === "Update connection config") {
|
|
469
|
-
if (conn.type === "sqlite") {
|
|
470
|
-
const path = await vscode.window.showInputBox({ prompt: "SQLite file path", placeHolder: "/path/to/database.db" });
|
|
471
|
-
if (path === undefined) return;
|
|
472
|
-
if (path) connectionConfig = { type: "sqlite", path };
|
|
473
|
-
} else {
|
|
474
|
-
const connStr = await vscode.window.showInputBox({ prompt: "PostgreSQL connection string", placeHolder: "postgres://user:pass@host:5432/dbname" });
|
|
475
|
-
if (connStr === undefined) return;
|
|
476
|
-
if (connStr) connectionConfig = { type: "postgres", connectionString: connStr };
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Group, color, readonly
|
|
481
|
-
const groupName = await vscode.window.showInputBox({ prompt: "Group name", value: conn.group_name ?? "" });
|
|
482
|
-
if (groupName === undefined) return;
|
|
483
|
-
|
|
484
|
-
const COLOR_OPTIONS = ["None", "#ef4444", "#f97316", "#eab308", "#22c55e", "#06b6d4", "#3b82f6", "#8b5cf6", "#ec4899"];
|
|
485
|
-
const color = await vscode.window.showQuickPick(COLOR_OPTIONS, { placeHolder: `Current color: ${conn.color ?? "None"}` });
|
|
486
|
-
if (color === undefined) return;
|
|
487
|
-
|
|
488
|
-
const readonlyChoice = await vscode.window.showQuickPick(["Yes (recommended)", "No"], {
|
|
489
|
-
placeHolder: `Readonly? (current: ${conn.readonly === 1 ? "Yes" : "No"})`,
|
|
490
|
-
});
|
|
491
|
-
if (readonlyChoice === undefined) return;
|
|
492
|
-
|
|
493
|
-
// Update via API
|
|
494
|
-
try {
|
|
495
|
-
const body: Record<string, unknown> = {
|
|
496
|
-
name: name || conn.name,
|
|
497
|
-
groupName: groupName || null,
|
|
498
|
-
color: color === "None" ? null : color,
|
|
499
|
-
readonly: readonlyChoice.startsWith("Yes") ? 1 : 0,
|
|
500
|
-
};
|
|
501
|
-
if (connectionConfig) body.connectionConfig = connectionConfig;
|
|
502
|
-
|
|
503
|
-
const res = await fetch(`${baseUrl}/api/db/connections/${connectionId}`, {
|
|
504
|
-
method: "PUT",
|
|
505
|
-
headers: { "Content-Type": "application/json" },
|
|
506
|
-
body: JSON.stringify(body),
|
|
507
|
-
});
|
|
508
|
-
const json = await res.json() as { ok: boolean; error?: string };
|
|
509
|
-
if (json.ok) {
|
|
510
|
-
await vscode.window.showInformationMessage(`Connection "${name || conn.name}" updated`);
|
|
511
|
-
treeProvider.refresh();
|
|
512
|
-
} else {
|
|
513
|
-
await vscode.window.showErrorMessage(json.error ?? "Failed to update connection");
|
|
514
|
-
}
|
|
515
|
-
} catch (e) {
|
|
516
|
-
await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// ---------------------------------------------------------------------------
|
|
521
|
-
// Export Connections — copy JSON to clipboard via showInformationMessage
|
|
522
|
-
// ---------------------------------------------------------------------------
|
|
523
|
-
|
|
524
|
-
async function exportConnections(vscode: VscodeApi): Promise<void> {
|
|
525
|
-
try {
|
|
526
|
-
const res = await fetch(`${baseUrl}/api/db/connections/export`);
|
|
527
|
-
const json = await res.json() as { ok: boolean; data?: unknown };
|
|
528
|
-
if (!json.ok || !json.data) {
|
|
529
|
-
await vscode.window.showErrorMessage("Failed to export connections");
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
// In extension context, we can't write to clipboard directly — show data as info
|
|
533
|
-
const data = json.data as { connections: unknown[] };
|
|
534
|
-
await vscode.window.showInformationMessage(`Exported ${data.connections.length} connection(s). Use built-in UI for file export.`);
|
|
535
|
-
} catch (e) {
|
|
536
|
-
await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// ---------------------------------------------------------------------------
|
|
541
|
-
// Import Connections — paste JSON via InputBox
|
|
542
|
-
// ---------------------------------------------------------------------------
|
|
543
|
-
|
|
544
|
-
async function importConnections(
|
|
545
|
-
vscode: VscodeApi,
|
|
546
|
-
treeProvider: ConnectionTreeProvider,
|
|
547
|
-
): Promise<void> {
|
|
548
|
-
const jsonStr = await vscode.window.showInputBox({
|
|
549
|
-
prompt: "Paste connections JSON (from export)",
|
|
550
|
-
placeHolder: '{"connections": [...]}',
|
|
551
|
-
});
|
|
552
|
-
if (!jsonStr) return;
|
|
553
|
-
|
|
554
|
-
try {
|
|
555
|
-
const data = JSON.parse(jsonStr);
|
|
556
|
-
const conns = data.connections ?? data;
|
|
557
|
-
if (!Array.isArray(conns)) {
|
|
558
|
-
await vscode.window.showErrorMessage("Invalid format: expected connections array");
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
const res = await fetch(`${baseUrl}/api/db/connections/import`, {
|
|
562
|
-
method: "POST",
|
|
563
|
-
headers: { "Content-Type": "application/json" },
|
|
564
|
-
body: JSON.stringify({ connections: conns }),
|
|
565
|
-
});
|
|
566
|
-
const json = await res.json() as { ok: boolean; data?: { imported: number; skipped: number; errors: string[] } };
|
|
567
|
-
if (json.ok && json.data) {
|
|
568
|
-
let msg = `Imported ${json.data.imported} connection(s)`;
|
|
569
|
-
if (json.data.skipped > 0) msg += `, ${json.data.skipped} skipped`;
|
|
570
|
-
await vscode.window.showInformationMessage(msg);
|
|
571
|
-
treeProvider.refresh();
|
|
572
|
-
} else {
|
|
573
|
-
await vscode.window.showErrorMessage("Import failed");
|
|
574
|
-
}
|
|
575
|
-
} catch (e) {
|
|
576
|
-
await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
577
|
-
}
|
|
578
|
-
}
|