@hienlh/ppm 0.9.3 → 0.9.4
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/CHANGELOG.md +10 -0
- package/dist/web/assets/{browser-tab-LFNnCzgB.js → browser-tab-B8EBpCT6.js} +1 -1
- package/dist/web/assets/{chat-tab-rYBo5Mff.js → chat-tab-DVuWNtNl.js} +1 -1
- package/dist/web/assets/{code-editor-BdM11-0K.js → code-editor-Bre-_HLS.js} +1 -1
- package/dist/web/assets/{database-viewer-CINo6teP.js → database-viewer-As9Pwu58.js} +1 -1
- package/dist/web/assets/{diff-viewer-_MPL-DRu.js → diff-viewer-DTw_Cqpy.js} +1 -1
- package/dist/web/assets/{extension-webview-BU1T2a8n.js → extension-webview-CgobD-e5.js} +1 -1
- package/dist/web/assets/{git-graph-Dde-j8cK.js → git-graph-CA1TsLfJ.js} +1 -1
- package/dist/web/assets/index-Dx21KBME.css +2 -0
- package/dist/web/assets/{index-CyXEMb4g.js → index-iaC9D6YI.js} +4 -4
- package/dist/web/assets/keybindings-store-BzWEhrZs.js +1 -0
- package/dist/web/assets/{markdown-renderer-Djgmbi23.js → markdown-renderer-zGERfKXC.js} +1 -1
- package/dist/web/assets/{postgres-viewer-FCpA6nh4.js → postgres-viewer-q-a11Zs_.js} +1 -1
- package/dist/web/assets/{settings-tab-Y37tD1kM.js → settings-tab-9ISb6u9A.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-Cjl4uXyo.js → sqlite-viewer-CEfqfnpC.js} +1 -1
- package/dist/web/assets/{terminal-tab-CnYqGghP.js → terminal-tab-BGthbegR.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-DGjkK3eO.js → use-monaco-theme-0QrlcbfM.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/packages/ext-database/package.json +9 -2
- package/packages/ext-database/src/connection-tree.ts +64 -5
- package/packages/ext-database/src/extension.ts +234 -2
- package/packages/ext-database/src/query-panel.ts +29 -16
- package/packages/ext-database/src/table-viewer-panel.ts +16 -6
- package/src/web/components/layout/mobile-nav.tsx +5 -5
- package/dist/web/assets/index-BkidPsSC.css +0 -2
- package/dist/web/assets/keybindings-store-6_p_JT0B.js +0 -1
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
interface ConnectionNode {
|
|
7
7
|
id: string;
|
|
8
8
|
name: string;
|
|
9
|
-
type: "connection" | "table" | "column";
|
|
9
|
+
type: "connection" | "group" | "table" | "column";
|
|
10
10
|
connectionId?: number;
|
|
11
11
|
connectionName?: string;
|
|
12
12
|
connectionType?: string;
|
|
13
13
|
connectionColor?: string | null;
|
|
14
|
+
connectionReadonly?: number;
|
|
15
|
+
groupName?: string | null;
|
|
14
16
|
schemaName?: string;
|
|
15
17
|
dataType?: string;
|
|
18
|
+
rowCount?: number;
|
|
19
|
+
children?: ConnectionNode[];
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
interface ApiConnection {
|
|
@@ -20,6 +24,8 @@ interface ApiConnection {
|
|
|
20
24
|
name: string;
|
|
21
25
|
type: string;
|
|
22
26
|
color: string | null;
|
|
27
|
+
readonly: number;
|
|
28
|
+
group_name: string | null;
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
interface ApiTable {
|
|
@@ -54,7 +60,8 @@ export class ConnectionTreeProvider {
|
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
async getChildren(element?: ConnectionNode): Promise<ConnectionNode[]> {
|
|
57
|
-
if (!element) return this.
|
|
63
|
+
if (!element) return this.getRootNodes();
|
|
64
|
+
if (element.type === "group") return element.children ?? [];
|
|
58
65
|
if (element.type === "connection") return this.getTables(element);
|
|
59
66
|
if (element.type === "table") return this.getColumns(element);
|
|
60
67
|
return [];
|
|
@@ -62,13 +69,26 @@ export class ConnectionTreeProvider {
|
|
|
62
69
|
|
|
63
70
|
getTreeItem(element: ConnectionNode): Record<string, unknown> {
|
|
64
71
|
const isConn = element.type === "connection";
|
|
72
|
+
const isGroup = element.type === "group";
|
|
65
73
|
const isTable = element.type === "table";
|
|
66
74
|
const isCol = element.type === "column";
|
|
67
75
|
|
|
76
|
+
// Table description: row count
|
|
77
|
+
let description: string | undefined;
|
|
78
|
+
if (isCol) description = element.dataType;
|
|
79
|
+
else if (isTable && element.rowCount !== undefined) description = `${element.rowCount.toLocaleString()} rows`;
|
|
80
|
+
|
|
81
|
+
// Connection badge: type + readonly
|
|
82
|
+
let badge: string | undefined;
|
|
83
|
+
if (isConn) {
|
|
84
|
+
badge = element.connectionType === "postgres" ? "PG" : "DB";
|
|
85
|
+
if (element.connectionReadonly === 1) badge += " 🔒";
|
|
86
|
+
}
|
|
87
|
+
|
|
68
88
|
return {
|
|
69
89
|
id: element.id,
|
|
70
90
|
label: element.name,
|
|
71
|
-
description
|
|
91
|
+
description,
|
|
72
92
|
collapsibleState: isCol ? "none" : "collapsed",
|
|
73
93
|
contextValue: element.type,
|
|
74
94
|
command: isTable ? "ppm-db.openViewer" : undefined,
|
|
@@ -76,13 +96,49 @@ export class ConnectionTreeProvider {
|
|
|
76
96
|
? [element.connectionId, element.connectionName ?? "Database", element.name, element.schemaName ?? "public"]
|
|
77
97
|
: undefined,
|
|
78
98
|
color: isConn ? (element.connectionColor ?? undefined) : undefined,
|
|
79
|
-
badge
|
|
99
|
+
badge,
|
|
80
100
|
actions: isConn ? [
|
|
81
101
|
{ icon: "refresh", tooltip: "Refresh tables", command: "ppm-db.refreshConnection", commandArgs: [element.connectionId] },
|
|
82
|
-
|
|
102
|
+
{ icon: "edit", tooltip: "Edit connection", command: "ppm-db.editConnection", commandArgs: [element.connectionId] },
|
|
103
|
+
{ icon: "trash", tooltip: "Delete connection", command: "ppm-db.deleteConnection", commandArgs: [element.connectionId, element.name] },
|
|
104
|
+
] : isGroup ? [] : undefined,
|
|
83
105
|
};
|
|
84
106
|
}
|
|
85
107
|
|
|
108
|
+
/** Build root tree: group nodes wrapping connection nodes, or flat if no groups */
|
|
109
|
+
private async getRootNodes(): Promise<ConnectionNode[]> {
|
|
110
|
+
const connections = await this.getConnections();
|
|
111
|
+
// Group by group_name
|
|
112
|
+
const groups = new Map<string, ConnectionNode[]>();
|
|
113
|
+
for (const conn of connections) {
|
|
114
|
+
const key = conn.groupName ?? "__ungrouped__";
|
|
115
|
+
const list = groups.get(key) ?? [];
|
|
116
|
+
list.push(conn);
|
|
117
|
+
groups.set(key, list);
|
|
118
|
+
}
|
|
119
|
+
// If only one group (ungrouped), return connections flat
|
|
120
|
+
if (groups.size <= 1 && groups.has("__ungrouped__")) {
|
|
121
|
+
return connections;
|
|
122
|
+
}
|
|
123
|
+
// Build group nodes
|
|
124
|
+
const result: ConnectionNode[] = [];
|
|
125
|
+
const sortedKeys = Array.from(groups.keys()).sort((a, b) => {
|
|
126
|
+
if (a === "__ungrouped__") return 1;
|
|
127
|
+
if (b === "__ungrouped__") return -1;
|
|
128
|
+
return a.localeCompare(b);
|
|
129
|
+
});
|
|
130
|
+
for (const key of sortedKeys) {
|
|
131
|
+
const label = key === "__ungrouped__" ? "Ungrouped" : key;
|
|
132
|
+
result.push({
|
|
133
|
+
id: `group:${key}`,
|
|
134
|
+
name: label,
|
|
135
|
+
type: "group",
|
|
136
|
+
children: groups.get(key) ?? [],
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
86
142
|
private async getConnections(): Promise<ConnectionNode[]> {
|
|
87
143
|
try {
|
|
88
144
|
const res = await fetch(`${this.baseUrl}/api/db/connections`);
|
|
@@ -95,6 +151,8 @@ export class ConnectionTreeProvider {
|
|
|
95
151
|
connectionId: c.id,
|
|
96
152
|
connectionType: c.type,
|
|
97
153
|
connectionColor: c.color,
|
|
154
|
+
connectionReadonly: c.readonly,
|
|
155
|
+
groupName: c.group_name,
|
|
98
156
|
}));
|
|
99
157
|
} catch {
|
|
100
158
|
return [];
|
|
@@ -114,6 +172,7 @@ export class ConnectionTreeProvider {
|
|
|
114
172
|
connectionName: conn.name,
|
|
115
173
|
connectionType: conn.connectionType,
|
|
116
174
|
schemaName: t.schema,
|
|
175
|
+
rowCount: t.rowCount,
|
|
117
176
|
}));
|
|
118
177
|
} catch {
|
|
119
178
|
return [];
|
|
@@ -96,6 +96,67 @@ export function activate(context: ExtensionContext, vscode: VscodeApi): void {
|
|
|
96
96
|
}),
|
|
97
97
|
);
|
|
98
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
|
+
|
|
99
160
|
// --- Status Bar ---
|
|
100
161
|
const statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 10);
|
|
101
162
|
statusItem.text = "DB";
|
|
@@ -297,6 +358,27 @@ function openQueryPanel(
|
|
|
297
358
|
// Add Connection — collect info via QuickPick + InputBox, then POST to API
|
|
298
359
|
// ---------------------------------------------------------------------------
|
|
299
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
|
+
|
|
300
382
|
async function addConnection(
|
|
301
383
|
vscode: VscodeApi,
|
|
302
384
|
treeProvider: ConnectionTreeProvider,
|
|
@@ -326,12 +408,16 @@ async function addConnection(
|
|
|
326
408
|
connectionConfig = { type: "postgres", connectionString: connStr };
|
|
327
409
|
}
|
|
328
410
|
|
|
329
|
-
// 4.
|
|
411
|
+
// 4. Group, color, readonly
|
|
412
|
+
const extras = await collectConnectionExtras(vscode);
|
|
413
|
+
if (extras === null) return;
|
|
414
|
+
|
|
415
|
+
// 5. Create via API
|
|
330
416
|
try {
|
|
331
417
|
const res = await fetch(`${baseUrl}/api/db/connections`, {
|
|
332
418
|
method: "POST",
|
|
333
419
|
headers: { "Content-Type": "application/json" },
|
|
334
|
-
body: JSON.stringify({ type, name, connectionConfig }),
|
|
420
|
+
body: JSON.stringify({ type, name, connectionConfig, ...extras }),
|
|
335
421
|
});
|
|
336
422
|
const json = await res.json() as { ok: boolean; error?: string };
|
|
337
423
|
if (json.ok) {
|
|
@@ -344,3 +430,149 @@ async function addConnection(
|
|
|
344
430
|
await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
345
431
|
}
|
|
346
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
|
+
}
|
|
@@ -15,25 +15,38 @@ export function getQueryPanelHtml(connectionName: string, tableName?: string): s
|
|
|
15
15
|
<head>
|
|
16
16
|
<meta charset="utf-8">
|
|
17
17
|
<style>
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
19
|
+
:root {
|
|
20
|
+
--bg: #ffffff; --surface: #f4f4f5; --text: #09090b; --subtext: #71717a; --subtle: #a1a1aa;
|
|
21
|
+
--border: #e4e4e7; --border2: #d4d4d8; --blue: #3b82f6; --red: #ef4444; --green: #22c55e;
|
|
22
|
+
--surface-hover: #f4f4f5;
|
|
23
|
+
}
|
|
24
|
+
@media (prefers-color-scheme: dark) {
|
|
25
|
+
:root {
|
|
26
|
+
--bg: #09090b; --surface: #18181b; --text: #fafafa; --subtext: #a1a1aa; --subtle: #52525b;
|
|
27
|
+
--border: #27272a; --border2: #3f3f46; --blue: #3b82f6; --red: #ef4444; --green: #22c55e;
|
|
28
|
+
--surface-hover: #27272a;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: var(--bg); color: var(--text); padding: 12px; font-size: 13px; }
|
|
20
32
|
.header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
|
21
33
|
.header h3 { font-size: 14px; font-weight: 600; }
|
|
22
|
-
.header .badge { background:
|
|
23
|
-
textarea { width: 100%; height: 80px; background:
|
|
24
|
-
textarea:focus { outline: none; border-color:
|
|
34
|
+
.header .badge { background: var(--surface); padding: 2px 8px; border-radius: 4px; font-size: 11px; color: var(--subtext); }
|
|
35
|
+
textarea { width: 100%; height: 80px; background: var(--surface); border: 1px solid var(--border2); border-radius: 6px; color: var(--text); padding: 8px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; resize: vertical; }
|
|
36
|
+
textarea:focus { outline: none; border-color: var(--blue); }
|
|
25
37
|
.actions { display: flex; gap: 8px; margin: 8px 0; }
|
|
26
|
-
button { background:
|
|
27
|
-
button:hover {
|
|
28
|
-
button.secondary { background:
|
|
29
|
-
.status { font-size: 11px; color:
|
|
30
|
-
.error { color:
|
|
38
|
+
button { background: var(--blue); color: #fff; border: none; padding: 6px 16px; border-radius: 4px; font-size: 12px; font-weight: 600; cursor: pointer; }
|
|
39
|
+
button:hover { opacity: 0.9; }
|
|
40
|
+
button.secondary { background: var(--surface); color: var(--text); }
|
|
41
|
+
.status { font-size: 11px; color: var(--subtext); margin: 4px 0; }
|
|
42
|
+
.error { color: var(--red); background: var(--surface); padding: 8px; border-radius: 4px; margin: 8px 0; font-size: 12px; }
|
|
31
43
|
table { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 12px; }
|
|
32
|
-
th { background:
|
|
33
|
-
td { padding: 4px 8px; border-bottom: 1px solid
|
|
34
|
-
tr:hover td { background:
|
|
35
|
-
.results { max-height: 60vh; overflow: auto; border: 1px solid
|
|
36
|
-
.empty { text-align: center; padding: 24px; color:
|
|
44
|
+
th { background: var(--surface); text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--border2); font-weight: 600; position: sticky; top: 0; color: var(--subtext); }
|
|
45
|
+
td { padding: 4px 8px; border-bottom: 1px solid var(--border); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
46
|
+
tr:hover td { background: var(--surface-hover); }
|
|
47
|
+
.results { max-height: 60vh; overflow: auto; border: 1px solid var(--border2); border-radius: 6px; }
|
|
48
|
+
.empty { text-align: center; padding: 24px; color: var(--subtle); }
|
|
49
|
+
.null-val { color: var(--subtle); font-style: italic; }
|
|
37
50
|
</style>
|
|
38
51
|
</head>
|
|
39
52
|
<body>
|
|
@@ -101,7 +114,7 @@ export function getQueryPanelHtml(connectionName: string, tableName?: string): s
|
|
|
101
114
|
html += '<tr>';
|
|
102
115
|
cols.forEach(c => {
|
|
103
116
|
const v = row[c];
|
|
104
|
-
html +=
|
|
117
|
+
html += v === null ? '<td class="null-val">NULL</td>' : '<td>' + esc(v) + '</td>';
|
|
105
118
|
});
|
|
106
119
|
html += '</tr>';
|
|
107
120
|
});
|
|
@@ -27,12 +27,22 @@ export function getTableViewerHtml(opts: TableViewerOptions): string {
|
|
|
27
27
|
<style>
|
|
28
28
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
29
29
|
:root {
|
|
30
|
-
--bg: #
|
|
31
|
-
--overlay0: #
|
|
32
|
-
--text: #
|
|
33
|
-
--border: #
|
|
34
|
-
--blue: #
|
|
35
|
-
--surface-hover: #
|
|
30
|
+
--bg: #ffffff; --surface: #f4f4f5; --mantle: #fafafa;
|
|
31
|
+
--overlay0: #71717a; --overlay1: #52525b;
|
|
32
|
+
--text: #09090b; --subtext: #71717a; --subtle: #a1a1aa;
|
|
33
|
+
--border: #e4e4e7; --border2: #d4d4d8;
|
|
34
|
+
--blue: #3b82f6; --green: #22c55e; --red: #ef4444; --yellow: #eab308;
|
|
35
|
+
--surface-hover: #f4f4f5;
|
|
36
|
+
}
|
|
37
|
+
@media (prefers-color-scheme: dark) {
|
|
38
|
+
:root {
|
|
39
|
+
--bg: #09090b; --surface: #18181b; --mantle: #09090b;
|
|
40
|
+
--overlay0: #71717a; --overlay1: #a1a1aa;
|
|
41
|
+
--text: #fafafa; --subtext: #a1a1aa; --subtle: #52525b;
|
|
42
|
+
--border: #27272a; --border2: #3f3f46;
|
|
43
|
+
--blue: #3b82f6; --green: #22c55e; --red: #ef4444; --yellow: #eab308;
|
|
44
|
+
--surface-hover: #27272a;
|
|
45
|
+
}
|
|
36
46
|
}
|
|
37
47
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); font-size: 13px; display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
|
|
38
48
|
|
|
@@ -116,8 +116,8 @@ export function MobileNav({ onMenuPress, onProjectsPress }: MobileNavProps) {
|
|
|
116
116
|
<div className="flex items-center h-12">
|
|
117
117
|
{/* Fixed section: Menu + Project + Add — curved right edge */}
|
|
118
118
|
<div className={cn(
|
|
119
|
-
"flex items-center shrink-0 bg-background relative z-10
|
|
120
|
-
canScrollLeft
|
|
119
|
+
"flex items-center shrink-0 bg-background relative z-10 transition-all duration-200",
|
|
120
|
+
canScrollLeft ? "rounded-r-2xl shadow-[6px_0_12px_-4px_rgba(0,0,0,0.12)]" : "border-r border-border",
|
|
121
121
|
)}>
|
|
122
122
|
<button onClick={onMenuPress} className="flex items-center justify-center size-12 shrink-0 text-text-secondary">
|
|
123
123
|
<Menu className="size-5" />
|
|
@@ -156,9 +156,9 @@ export function MobileNav({ onMenuPress, onProjectsPress }: MobileNavProps) {
|
|
|
156
156
|
</button>
|
|
157
157
|
</div>
|
|
158
158
|
|
|
159
|
-
{/* Tab list —
|
|
160
|
-
<div className="flex-1 min-w-0 relative flex items-center h-12">
|
|
161
|
-
<div ref={mobileScrollRef} className="flex-1 min-w-0 flex items-center h-12 overflow-x-auto scrollbar-none">
|
|
159
|
+
{/* Tab list — overlaps under curved edge so tabs slide beneath it */}
|
|
160
|
+
<div className="flex-1 min-w-0 relative flex items-center h-12 -ml-4">
|
|
161
|
+
<div ref={mobileScrollRef} className="flex-1 min-w-0 flex items-center h-12 overflow-x-auto scrollbar-none pl-4">
|
|
162
162
|
{tabs.map((tab) => {
|
|
163
163
|
const Icon = TAB_ICONS[tab.type];
|
|
164
164
|
const isActive = tab.id === activeTabId;
|