@hienlh/ppm 0.13.48 → 0.13.50
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 +8 -0
- 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-CILFIsuu.js} +2 -2
- package/dist/web/assets/{audio-preview-BFc4v5Tx.js.map → audio-preview-CILFIsuu.js.map} +1 -1
- package/dist/web/assets/{chat-tab-x1rBGHj5.js → chat-tab-DBYwH_Aa.js} +4 -4
- package/dist/web/assets/{chat-tab-x1rBGHj5.js.map → chat-tab-DBYwH_Aa.js.map} +1 -1
- package/dist/web/assets/{code-editor-BgyAZcQX.js → code-editor-MXnkYRLp.js} +3 -3
- package/dist/web/assets/{code-editor-BgyAZcQX.js.map → code-editor-MXnkYRLp.js.map} +1 -1
- package/dist/web/assets/{conflict-editor-Dhc1lem7.js → conflict-editor-C6wH5wV6.js} +2 -2
- package/dist/web/assets/{conflict-editor-Dhc1lem7.js.map → conflict-editor-C6wH5wV6.js.map} +1 -1
- package/dist/web/assets/{database-viewer-DnnjO38W.js → database-viewer-BjUruZLv.js} +2 -2
- package/dist/web/assets/{database-viewer-DnnjO38W.js.map → database-viewer-BjUruZLv.js.map} +1 -1
- package/dist/web/assets/{diff-viewer-B6q9wXD6.js → diff-viewer-B_nU7bQi.js} +2 -2
- package/dist/web/assets/{diff-viewer-B6q9wXD6.js.map → diff-viewer-B_nU7bQi.js.map} +1 -1
- package/dist/web/assets/{extension-webview-CMBEb4FF.js → extension-webview-B56ZfvoD.js} +2 -2
- package/dist/web/assets/{extension-webview-CMBEb4FF.js.map → extension-webview-B56ZfvoD.js.map} +1 -1
- package/dist/web/assets/{glide-data-grid-BLcBAxgp.js → glide-data-grid-D-qQqqp7.js} +2 -2
- package/dist/web/assets/{glide-data-grid-BLcBAxgp.js.map → glide-data-grid-D-qQqqp7.js.map} +1 -1
- package/dist/web/assets/{image-preview-YpWk7AOb.js → image-preview-Dc6AiqYX.js} +2 -2
- package/dist/web/assets/{image-preview-YpWk7AOb.js.map → image-preview-Dc6AiqYX.js.map} +1 -1
- package/dist/web/assets/{index-hxAGD2rx.js → index-8_rE2Q1-.js} +6 -6
- package/dist/web/assets/{index-hxAGD2rx.js.map → index-8_rE2Q1-.js.map} +1 -1
- package/dist/web/assets/keybindings-store-COJD5O6M.js +1 -0
- package/dist/web/assets/{markdown-renderer-BAAmsTL9.js → markdown-renderer-CNQ8I0Dk.js} +2 -2
- package/dist/web/assets/{markdown-renderer-BAAmsTL9.js.map → markdown-renderer-CNQ8I0Dk.js.map} +1 -1
- package/dist/web/assets/notification-store-BiZaLXop.js +1 -0
- package/dist/web/assets/{panel-store-Dy8-7E_g.js → panel-store-C8wwxBpn.js} +2 -2
- package/dist/web/assets/{panel-store-Dy8-7E_g.js.map → panel-store-C8wwxBpn.js.map} +1 -1
- package/dist/web/assets/{pdf-preview-DWe7ARXI.js → pdf-preview-zs9QdgDp.js} +2 -2
- package/dist/web/assets/{pdf-preview-DWe7ARXI.js.map → pdf-preview-zs9QdgDp.js.map} +1 -1
- package/dist/web/assets/{port-forwarding-tab-R5V7zkKO.js → port-forwarding-tab-sArYx1nt.js} +2 -2
- package/dist/web/assets/{port-forwarding-tab-R5V7zkKO.js.map → port-forwarding-tab-sArYx1nt.js.map} +1 -1
- package/dist/web/assets/{postgres-viewer-AxCUyDQP.js → postgres-viewer-khk7N7cd.js} +2 -2
- package/dist/web/assets/{postgres-viewer-AxCUyDQP.js.map → postgres-viewer-khk7N7cd.js.map} +1 -1
- package/dist/web/assets/{settings-tab-CJ6w6q2s.js → settings-tab-CGWhVzQm.js} +1 -1
- package/dist/web/assets/{sql-query-editor-DhZvNbKv.js → sql-query-editor-B5Ndypxp.js} +2 -2
- package/dist/web/assets/{sql-query-editor-DhZvNbKv.js.map → sql-query-editor-B5Ndypxp.js.map} +1 -1
- package/dist/web/assets/{sqlite-viewer-Qv8TlhPb.js → sqlite-viewer-BkpONSGa.js} +2 -2
- package/dist/web/assets/{sqlite-viewer-Qv8TlhPb.js.map → sqlite-viewer-BkpONSGa.js.map} +1 -1
- package/dist/web/assets/{tab-store-Dtg1_qL0.js → tab-store-CNas5Ny8.js} +2 -2
- package/dist/web/assets/{tab-store-Dtg1_qL0.js.map → tab-store-CNas5Ny8.js.map} +1 -1
- package/dist/web/assets/{terminal-tab-9hLQtUUT.js → terminal-tab-BgMCsdeN.js} +2 -2
- package/dist/web/assets/{terminal-tab-9hLQtUUT.js.map → terminal-tab-BgMCsdeN.js.map} +1 -1
- package/dist/web/assets/{video-preview-C-tj7tok.js → video-preview-w8ZAy8av.js} +2 -2
- package/dist/web/assets/{video-preview-C-tj7tok.js.map → video-preview-w8ZAy8av.js.map} +1 -1
- package/dist/web/index.html +3 -3
- 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/src/web/stores/panel-store.ts +5 -0
- 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,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WebviewPanel for SQL query editor + results display.
|
|
3
|
-
* Communicates with the extension via postMessage.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
function escHtml(s: string): string {
|
|
7
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** Create the HTML content for the query webview panel */
|
|
11
|
-
export function getQueryPanelHtml(connectionName: string, tableName?: string): string {
|
|
12
|
-
const initialQuery = tableName ? `SELECT * FROM ${escHtml(tableName)} LIMIT 100;` : "";
|
|
13
|
-
return `<!DOCTYPE html>
|
|
14
|
-
<html>
|
|
15
|
-
<head>
|
|
16
|
-
<meta charset="utf-8">
|
|
17
|
-
<style>
|
|
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; }
|
|
32
|
-
.header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
|
33
|
-
.header h3 { font-size: 14px; font-weight: 600; }
|
|
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); }
|
|
37
|
-
.actions { display: flex; gap: 8px; margin: 8px 0; }
|
|
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; }
|
|
43
|
-
table { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 12px; }
|
|
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; }
|
|
50
|
-
</style>
|
|
51
|
-
</head>
|
|
52
|
-
<body>
|
|
53
|
-
<div class="header">
|
|
54
|
-
<h3>Query</h3>
|
|
55
|
-
<span class="badge">${escHtml(connectionName)}</span>
|
|
56
|
-
</div>
|
|
57
|
-
<textarea id="sql" placeholder="Enter SQL query...">${initialQuery}</textarea>
|
|
58
|
-
<div class="actions">
|
|
59
|
-
<button id="run">Run Query</button>
|
|
60
|
-
<button class="secondary" id="clear">Clear</button>
|
|
61
|
-
</div>
|
|
62
|
-
<div id="status" class="status"></div>
|
|
63
|
-
<div id="error" style="display:none"></div>
|
|
64
|
-
<div id="results"></div>
|
|
65
|
-
<script>
|
|
66
|
-
const vscode = acquireVsCodeApi();
|
|
67
|
-
const sqlEl = document.getElementById('sql');
|
|
68
|
-
const statusEl = document.getElementById('status');
|
|
69
|
-
const errorEl = document.getElementById('error');
|
|
70
|
-
const resultsEl = document.getElementById('results');
|
|
71
|
-
|
|
72
|
-
document.getElementById('run').addEventListener('click', () => {
|
|
73
|
-
const sql = sqlEl.value.trim();
|
|
74
|
-
if (!sql) return;
|
|
75
|
-
statusEl.textContent = 'Running...';
|
|
76
|
-
errorEl.style.display = 'none';
|
|
77
|
-
resultsEl.innerHTML = '';
|
|
78
|
-
vscode.postMessage({ type: 'executeQuery', sql });
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
document.getElementById('clear').addEventListener('click', () => {
|
|
82
|
-
sqlEl.value = '';
|
|
83
|
-
statusEl.textContent = '';
|
|
84
|
-
errorEl.style.display = 'none';
|
|
85
|
-
resultsEl.innerHTML = '';
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
sqlEl.addEventListener('keydown', (e) => {
|
|
89
|
-
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
90
|
-
document.getElementById('run').click();
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
window.addEventListener('message', (event) => {
|
|
95
|
-
const msg = event.data;
|
|
96
|
-
if (msg.type === 'queryResult') {
|
|
97
|
-
if (msg.changeType === 'modify') {
|
|
98
|
-
statusEl.textContent = (msg.rowsAffected ?? 0) + ' row(s) affected' + (msg.duration ? ' in ' + msg.duration + 'ms' : '');
|
|
99
|
-
resultsEl.innerHTML = '';
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const rows = msg.rows || [];
|
|
103
|
-
statusEl.textContent = rows.length + ' row(s) returned' + (msg.duration ? ' in ' + msg.duration + 'ms' : '');
|
|
104
|
-
if (rows.length === 0) {
|
|
105
|
-
resultsEl.innerHTML = '<div class="empty">No results</div>';
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
|
109
|
-
const cols = msg.columns || Object.keys(rows[0]);
|
|
110
|
-
let html = '<div class="results"><table><thead><tr>';
|
|
111
|
-
cols.forEach(c => { html += '<th>' + esc(c) + '</th>'; });
|
|
112
|
-
html += '</tr></thead><tbody>';
|
|
113
|
-
rows.forEach(row => {
|
|
114
|
-
html += '<tr>';
|
|
115
|
-
cols.forEach(c => {
|
|
116
|
-
const v = row[c];
|
|
117
|
-
html += v === null ? '<td class="null-val">NULL</td>' : '<td>' + esc(v) + '</td>';
|
|
118
|
-
});
|
|
119
|
-
html += '</tr>';
|
|
120
|
-
});
|
|
121
|
-
html += '</tbody></table></div>';
|
|
122
|
-
resultsEl.innerHTML = html;
|
|
123
|
-
} else if (msg.type === 'queryError') {
|
|
124
|
-
statusEl.textContent = '';
|
|
125
|
-
errorEl.textContent = msg.error;
|
|
126
|
-
errorEl.style.display = 'block';
|
|
127
|
-
errorEl.className = 'error';
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
</script>
|
|
131
|
-
</body>
|
|
132
|
-
</html>`;
|
|
133
|
-
}
|
|
@@ -1,420 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Full-featured table viewer webview panel.
|
|
3
|
-
* Replicates ~90% of the built-in DatabaseViewer:
|
|
4
|
-
* - Toolbar with connection/table name, refresh, SQL toggle
|
|
5
|
-
* - Data grid with sticky headers, inline cell editing (double-click)
|
|
6
|
-
* - Pagination (prev/next, page X / total)
|
|
7
|
-
* - Toggleable SQL query panel with execute (Cmd+Enter)
|
|
8
|
-
* - NULL styling, row hover, loading states
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
function esc(s: string): string {
|
|
12
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface TableViewerOptions {
|
|
16
|
-
connectionId: number;
|
|
17
|
-
connectionName: string;
|
|
18
|
-
tableName: string;
|
|
19
|
-
schemaName: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function getTableViewerHtml(opts: TableViewerOptions): string {
|
|
23
|
-
return `<!DOCTYPE html>
|
|
24
|
-
<html>
|
|
25
|
-
<head>
|
|
26
|
-
<meta charset="utf-8">
|
|
27
|
-
<style>
|
|
28
|
-
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
29
|
-
:root {
|
|
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
|
-
}
|
|
46
|
-
}
|
|
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; }
|
|
48
|
-
|
|
49
|
-
/* Toolbar */
|
|
50
|
-
.toolbar { display: flex; align-items: center; gap: 8px; padding: 6px 12px; border-bottom: 1px solid var(--border); background: var(--bg); flex-shrink: 0; }
|
|
51
|
-
.toolbar .icon { width: 14px; height: 14px; color: var(--overlay0); }
|
|
52
|
-
.toolbar .conn-name { font-size: 12px; color: var(--subtext); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
53
|
-
.toolbar .separator { color: var(--subtle); font-size: 12px; }
|
|
54
|
-
.toolbar .table-name { font-size: 12px; color: var(--subtext); }
|
|
55
|
-
.toolbar .spacer { flex: 1; }
|
|
56
|
-
.toolbar button { background: none; border: none; cursor: pointer; padding: 4px; border-radius: 4px; color: var(--subtext); transition: color 0.15s, background 0.15s; display: flex; align-items: center; justify-content: center; }
|
|
57
|
-
.toolbar button:hover { color: var(--text); background: var(--surface-hover); }
|
|
58
|
-
.toolbar button.active { background: var(--border); color: var(--text); }
|
|
59
|
-
.toolbar .btn-text { font-size: 12px; padding: 4px 8px; font-weight: 500; }
|
|
60
|
-
|
|
61
|
-
/* Grid container */
|
|
62
|
-
.grid-container { flex: 1; overflow: hidden; display: flex; flex-direction: column; min-height: 0; }
|
|
63
|
-
.grid-scroll { flex: 1; overflow: auto; min-height: 0; }
|
|
64
|
-
|
|
65
|
-
/* Table */
|
|
66
|
-
table { width: 100%; border-collapse: collapse; font-size: 12px; table-layout: auto; }
|
|
67
|
-
thead { position: sticky; top: 0; z-index: 10; }
|
|
68
|
-
th { background: var(--border); text-align: left; padding: 6px 8px; font-weight: 600; font-size: 11px; color: var(--subtext); white-space: nowrap; border-bottom: 1px solid var(--border2); }
|
|
69
|
-
th.pk { font-weight: 700; color: var(--yellow); }
|
|
70
|
-
td { padding: 4px 8px; border-bottom: 1px solid var(--border); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: default; }
|
|
71
|
-
tr:hover td { background: var(--surface-hover); }
|
|
72
|
-
td.null-val { color: var(--subtle); font-style: italic; }
|
|
73
|
-
td.editing { padding: 2px 4px; }
|
|
74
|
-
td.editing input { width: 100%; background: transparent; border: 1px solid var(--blue); border-radius: 3px; color: var(--text); padding: 2px 4px; font-size: 12px; font-family: inherit; outline: none; }
|
|
75
|
-
|
|
76
|
-
/* Pagination */
|
|
77
|
-
.pagination { display: flex; align-items: center; justify-content: space-between; padding: 6px 12px; border-top: 1px solid var(--border); background: var(--bg); flex-shrink: 0; font-size: 12px; color: var(--subtext); }
|
|
78
|
-
.pagination .page-controls { display: flex; align-items: center; gap: 8px; }
|
|
79
|
-
.pagination button { background: none; border: none; cursor: pointer; padding: 2px; border-radius: 3px; color: var(--subtext); display: flex; align-items: center; }
|
|
80
|
-
.pagination button:hover:not(:disabled) { color: var(--text); background: var(--surface-hover); }
|
|
81
|
-
.pagination button:disabled { opacity: 0.3; cursor: default; }
|
|
82
|
-
|
|
83
|
-
/* SQL panel */
|
|
84
|
-
.sql-panel { border-top: 1px solid var(--border); display: flex; flex-direction: column; height: 40%; flex-shrink: 0; overflow: hidden; }
|
|
85
|
-
.sql-panel.hidden { display: none; }
|
|
86
|
-
.sql-header { display: flex; align-items: flex-start; gap: 4px; border-bottom: 1px solid var(--border); background: var(--bg); }
|
|
87
|
-
.sql-header textarea { flex: 1; background: transparent; border: none; color: var(--text); padding: 8px; font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; font-size: 12px; resize: none; outline: none; max-height: 120px; min-height: 60px; }
|
|
88
|
-
.sql-header button { margin: 4px; padding: 6px; border-radius: 4px; background: var(--blue); color: var(--mantle); border: none; cursor: pointer; font-weight: 600; font-size: 12px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
|
89
|
-
.sql-header button:hover { opacity: 0.9; }
|
|
90
|
-
.sql-header button:disabled { opacity: 0.5; }
|
|
91
|
-
.sql-results { flex: 1; overflow: auto; font-size: 12px; min-height: 0; }
|
|
92
|
-
.sql-results .sql-error { padding: 8px 12px; color: var(--red); background: rgba(243,139,168,0.05); }
|
|
93
|
-
.sql-results .sql-modify { padding: 8px 12px; color: var(--green); }
|
|
94
|
-
.sql-results .sql-empty { padding: 8px 12px; color: var(--subtext); }
|
|
95
|
-
.sql-results table th { background: var(--border); }
|
|
96
|
-
|
|
97
|
-
/* Loading / empty states */
|
|
98
|
-
.state-msg { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--overlay0); font-size: 12px; gap: 8px; }
|
|
99
|
-
.spinner { width: 16px; height: 16px; border: 2px solid var(--border2); border-top-color: var(--blue); border-radius: 50%; animation: spin 0.6s linear infinite; }
|
|
100
|
-
@keyframes spin { to { transform: rotate(360deg); } }
|
|
101
|
-
.refresh-spin { animation: spin 0.6s linear infinite; }
|
|
102
|
-
|
|
103
|
-
/* SVG icons inline */
|
|
104
|
-
svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
|
105
|
-
</style>
|
|
106
|
-
</head>
|
|
107
|
-
<body>
|
|
108
|
-
<!-- Toolbar -->
|
|
109
|
-
<div class="toolbar">
|
|
110
|
-
<svg class="icon" viewBox="0 0 24 24"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19A9 3 0 0 0 21 19V5"/><path d="M3 12A9 3 0 0 0 21 12"/></svg>
|
|
111
|
-
<span class="conn-name">${esc(opts.connectionName)}</span>
|
|
112
|
-
<span class="separator">/</span>
|
|
113
|
-
<span class="table-name">${esc(opts.tableName)}</span>
|
|
114
|
-
<span class="spacer"></span>
|
|
115
|
-
<button id="btn-refresh" title="Refresh data">
|
|
116
|
-
<svg id="refresh-icon" viewBox="0 0 24 24"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
|
|
117
|
-
</button>
|
|
118
|
-
<button id="btn-sql" class="btn-text" title="Toggle SQL query panel">SQL</button>
|
|
119
|
-
</div>
|
|
120
|
-
|
|
121
|
-
<!-- Data grid -->
|
|
122
|
-
<div class="grid-container" id="grid-container">
|
|
123
|
-
<div class="grid-scroll" id="grid-scroll">
|
|
124
|
-
<div class="state-msg" id="loading-msg"><div class="spinner"></div> Loading…</div>
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
|
|
128
|
-
<!-- Pagination -->
|
|
129
|
-
<div class="pagination" id="pagination" style="display:none">
|
|
130
|
-
<span id="row-count">0 rows</span>
|
|
131
|
-
<div class="page-controls">
|
|
132
|
-
<button id="btn-prev" disabled>
|
|
133
|
-
<svg viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
|
|
134
|
-
</button>
|
|
135
|
-
<span id="page-info">1 / 1</span>
|
|
136
|
-
<button id="btn-next" disabled>
|
|
137
|
-
<svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
|
|
138
|
-
</button>
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
|
-
|
|
142
|
-
<!-- SQL panel -->
|
|
143
|
-
<div class="sql-panel hidden" id="sql-panel">
|
|
144
|
-
<div class="sql-header">
|
|
145
|
-
<textarea id="sql-input" placeholder="Enter SQL query… (Cmd+Enter to execute)" rows="3">SELECT * FROM ${esc(opts.schemaName !== "main" && opts.schemaName !== "public" ? opts.schemaName + "." : "")}${esc(opts.tableName)} LIMIT 100;</textarea>
|
|
146
|
-
<button id="btn-run-sql" title="Execute (Cmd+Enter)">
|
|
147
|
-
<svg viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
|
148
|
-
</button>
|
|
149
|
-
</div>
|
|
150
|
-
<div class="sql-results" id="sql-results"></div>
|
|
151
|
-
</div>
|
|
152
|
-
|
|
153
|
-
<script>
|
|
154
|
-
(function() {
|
|
155
|
-
const vscode = acquireVsCodeApi();
|
|
156
|
-
const connId = ${opts.connectionId};
|
|
157
|
-
const tableName = "${esc(opts.tableName).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}";
|
|
158
|
-
const schemaName = "${esc(opts.schemaName).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}";
|
|
159
|
-
|
|
160
|
-
// State
|
|
161
|
-
let currentPage = 1;
|
|
162
|
-
let totalRows = 0;
|
|
163
|
-
let pageLimit = 100;
|
|
164
|
-
let columns = [];
|
|
165
|
-
let rows = [];
|
|
166
|
-
let schema = []; // { name, type, nullable, pk, defaultValue }
|
|
167
|
-
let pkCol = null;
|
|
168
|
-
let editingCell = null; // { rowIdx, col }
|
|
169
|
-
let loading = true;
|
|
170
|
-
let sqlPanelOpen = false;
|
|
171
|
-
|
|
172
|
-
// Elements
|
|
173
|
-
const gridScroll = document.getElementById('grid-scroll');
|
|
174
|
-
const gridContainer = document.getElementById('grid-container');
|
|
175
|
-
const pagination = document.getElementById('pagination');
|
|
176
|
-
const rowCountEl = document.getElementById('row-count');
|
|
177
|
-
const pageInfoEl = document.getElementById('page-info');
|
|
178
|
-
const btnPrev = document.getElementById('btn-prev');
|
|
179
|
-
const btnNext = document.getElementById('btn-next');
|
|
180
|
-
const btnRefresh = document.getElementById('btn-refresh');
|
|
181
|
-
const refreshIcon = document.getElementById('refresh-icon');
|
|
182
|
-
const btnSql = document.getElementById('btn-sql');
|
|
183
|
-
const sqlPanel = document.getElementById('sql-panel');
|
|
184
|
-
const sqlInput = document.getElementById('sql-input');
|
|
185
|
-
const sqlResults = document.getElementById('sql-results');
|
|
186
|
-
const btnRunSql = document.getElementById('btn-run-sql');
|
|
187
|
-
|
|
188
|
-
// -- Request initial data --
|
|
189
|
-
vscode.postMessage({ type: 'init', connectionId: connId, tableName, schemaName });
|
|
190
|
-
|
|
191
|
-
// -- Toolbar actions --
|
|
192
|
-
btnRefresh.addEventListener('click', () => {
|
|
193
|
-
loading = true;
|
|
194
|
-
refreshIcon.classList.add('refresh-spin');
|
|
195
|
-
vscode.postMessage({ type: 'refresh' });
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
btnSql.addEventListener('click', () => {
|
|
199
|
-
sqlPanelOpen = !sqlPanelOpen;
|
|
200
|
-
sqlPanel.classList.toggle('hidden', !sqlPanelOpen);
|
|
201
|
-
btnSql.classList.toggle('active', sqlPanelOpen);
|
|
202
|
-
if (sqlPanelOpen) {
|
|
203
|
-
gridContainer.style.maxHeight = '60%';
|
|
204
|
-
} else {
|
|
205
|
-
gridContainer.style.maxHeight = '';
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// -- Pagination --
|
|
210
|
-
btnPrev.addEventListener('click', () => {
|
|
211
|
-
if (currentPage > 1) { currentPage--; fetchPage(); }
|
|
212
|
-
});
|
|
213
|
-
btnNext.addEventListener('click', () => {
|
|
214
|
-
const totalPages = Math.ceil(totalRows / pageLimit) || 1;
|
|
215
|
-
if (currentPage < totalPages) { currentPage++; fetchPage(); }
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
function fetchPage() {
|
|
219
|
-
loading = true;
|
|
220
|
-
renderGrid();
|
|
221
|
-
vscode.postMessage({ type: 'fetchPage', page: currentPage });
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// -- SQL --
|
|
225
|
-
btnRunSql.addEventListener('click', runSql);
|
|
226
|
-
sqlInput.addEventListener('keydown', (e) => {
|
|
227
|
-
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
228
|
-
e.preventDefault();
|
|
229
|
-
runSql();
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
function runSql() {
|
|
234
|
-
const sql = sqlInput.value.trim();
|
|
235
|
-
if (!sql) return;
|
|
236
|
-
btnRunSql.disabled = true;
|
|
237
|
-
sqlResults.innerHTML = '<div class="state-msg"><div class="spinner"></div></div>';
|
|
238
|
-
vscode.postMessage({ type: 'executeQuery', sql });
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// -- Render data grid --
|
|
242
|
-
function renderGrid() {
|
|
243
|
-
if (loading && rows.length === 0) {
|
|
244
|
-
gridScroll.innerHTML = '<div class="state-msg"><div class="spinner"></div> Loading…</div>';
|
|
245
|
-
pagination.style.display = 'none';
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
if (!columns.length) {
|
|
249
|
-
gridScroll.innerHTML = '<div class="state-msg">No data</div>';
|
|
250
|
-
pagination.style.display = 'none';
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
let html = '<table><thead><tr>';
|
|
255
|
-
columns.forEach(col => {
|
|
256
|
-
const info = schema.find(s => s.name === col);
|
|
257
|
-
const isPk = info && info.pk;
|
|
258
|
-
html += '<th' + (isPk ? ' class="pk"' : '') + '>' + escH(col) + '</th>';
|
|
259
|
-
});
|
|
260
|
-
html += '</tr></thead><tbody>';
|
|
261
|
-
|
|
262
|
-
if (rows.length === 0) {
|
|
263
|
-
html += '<tr><td colspan="' + columns.length + '" style="text-align:center;padding:32px;color:var(--overlay0)">No data</td></tr>';
|
|
264
|
-
} else {
|
|
265
|
-
rows.forEach((row, rowIdx) => {
|
|
266
|
-
html += '<tr>';
|
|
267
|
-
columns.forEach(col => {
|
|
268
|
-
const val = row[col];
|
|
269
|
-
const isEditing = editingCell && editingCell.rowIdx === rowIdx && editingCell.col === col;
|
|
270
|
-
if (isEditing) {
|
|
271
|
-
const editVal = val == null ? '' : String(val);
|
|
272
|
-
html += '<td class="editing"><input data-row="' + rowIdx + '" data-col="' + escH(col) + '" value="' + escH(editVal) + '" /></td>';
|
|
273
|
-
} else if (val == null) {
|
|
274
|
-
html += '<td class="null-val" data-row="' + rowIdx + '" data-col="' + escH(col) + '">NULL</td>';
|
|
275
|
-
} else {
|
|
276
|
-
html += '<td data-row="' + rowIdx + '" data-col="' + escH(col) + '" title="' + escH(String(val)) + '">' + escH(String(val)) + '</td>';
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
html += '</tr>';
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
html += '</tbody></table>';
|
|
283
|
-
gridScroll.innerHTML = html;
|
|
284
|
-
|
|
285
|
-
// Attach edit handlers
|
|
286
|
-
if (pkCol) {
|
|
287
|
-
gridScroll.querySelectorAll('td:not(.editing)').forEach(td => {
|
|
288
|
-
td.addEventListener('dblclick', () => {
|
|
289
|
-
const r = parseInt(td.getAttribute('data-row'));
|
|
290
|
-
const c = td.getAttribute('data-col');
|
|
291
|
-
if (isNaN(r) || !c) return;
|
|
292
|
-
editingCell = { rowIdx: r, col: c };
|
|
293
|
-
renderGrid();
|
|
294
|
-
// Focus the input
|
|
295
|
-
setTimeout(() => {
|
|
296
|
-
const inp = gridScroll.querySelector('input[data-row="' + r + '"][data-col="' + c + '"]');
|
|
297
|
-
if (inp) { inp.focus(); inp.select(); }
|
|
298
|
-
}, 10);
|
|
299
|
-
});
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Edit input handlers
|
|
304
|
-
gridScroll.querySelectorAll('td.editing input').forEach(inp => {
|
|
305
|
-
inp.addEventListener('blur', () => commitEdit(inp));
|
|
306
|
-
inp.addEventListener('keydown', (e) => {
|
|
307
|
-
if (e.key === 'Enter') { e.preventDefault(); commitEdit(inp); }
|
|
308
|
-
if (e.key === 'Escape') { editingCell = null; renderGrid(); }
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
// Update pagination
|
|
313
|
-
const totalPages = Math.ceil(totalRows / pageLimit) || 1;
|
|
314
|
-
pagination.style.display = 'flex';
|
|
315
|
-
rowCountEl.textContent = totalRows.toLocaleString() + ' rows';
|
|
316
|
-
pageInfoEl.textContent = currentPage + ' / ' + totalPages;
|
|
317
|
-
btnPrev.disabled = currentPage <= 1;
|
|
318
|
-
btnNext.disabled = currentPage >= totalPages;
|
|
319
|
-
refreshIcon.classList.remove('refresh-spin');
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function commitEdit(inp) {
|
|
323
|
-
if (!editingCell || !pkCol) return;
|
|
324
|
-
const rowIdx = editingCell.rowIdx;
|
|
325
|
-
const col = editingCell.col;
|
|
326
|
-
const newVal = inp.value;
|
|
327
|
-
const row = rows[rowIdx];
|
|
328
|
-
if (!row) { editingCell = null; renderGrid(); return; }
|
|
329
|
-
const oldVal = row[col];
|
|
330
|
-
editingCell = null;
|
|
331
|
-
if (String(oldVal ?? '') !== newVal) {
|
|
332
|
-
vscode.postMessage({
|
|
333
|
-
type: 'updateCell',
|
|
334
|
-
pkColumn: pkCol,
|
|
335
|
-
pkValue: row[pkCol],
|
|
336
|
-
column: col,
|
|
337
|
-
value: newVal === '' ? null : newVal,
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
renderGrid();
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// -- Render SQL results --
|
|
344
|
-
function renderSqlResult(msg) {
|
|
345
|
-
btnRunSql.disabled = false;
|
|
346
|
-
if (msg.type === 'queryError') {
|
|
347
|
-
sqlResults.innerHTML = '<div class="sql-error">' + escH(msg.error) + '</div>';
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
if (msg.changeType === 'modify') {
|
|
351
|
-
sqlResults.innerHTML = '<div class="sql-modify">Query executed. ' + (msg.rowsAffected ?? 0) + ' row(s) affected.' + (msg.duration ? ' (' + msg.duration + 'ms)' : '') + '</div>';
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
// select
|
|
355
|
-
if (!msg.rows || msg.rows.length === 0) {
|
|
356
|
-
sqlResults.innerHTML = '<div class="sql-empty">No results' + (msg.duration ? ' (' + msg.duration + 'ms)' : '') + '</div>';
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
const cols = msg.columns || Object.keys(msg.rows[0]);
|
|
360
|
-
let html = '<table><thead><tr>';
|
|
361
|
-
cols.forEach(c => { html += '<th>' + escH(c) + '</th>'; });
|
|
362
|
-
html += '</tr></thead><tbody>';
|
|
363
|
-
msg.rows.forEach(row => {
|
|
364
|
-
html += '<tr>';
|
|
365
|
-
cols.forEach(c => {
|
|
366
|
-
const v = row[c];
|
|
367
|
-
html += v == null
|
|
368
|
-
? '<td class="null-val">NULL</td>'
|
|
369
|
-
: '<td title="' + escH(String(v)) + '">' + escH(String(v)) + '</td>';
|
|
370
|
-
});
|
|
371
|
-
html += '</tr>';
|
|
372
|
-
});
|
|
373
|
-
html += '</tbody></table>';
|
|
374
|
-
sqlResults.innerHTML = html;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// -- Message handler --
|
|
378
|
-
window.addEventListener('message', (event) => {
|
|
379
|
-
const msg = event.data;
|
|
380
|
-
switch (msg.type) {
|
|
381
|
-
case 'tableData':
|
|
382
|
-
columns = msg.columns || [];
|
|
383
|
-
rows = msg.rows || [];
|
|
384
|
-
totalRows = msg.total ?? rows.length;
|
|
385
|
-
pageLimit = msg.limit ?? 100;
|
|
386
|
-
currentPage = msg.page ?? currentPage;
|
|
387
|
-
schema = msg.schema || [];
|
|
388
|
-
pkCol = null;
|
|
389
|
-
for (const s of schema) { if (s.pk) { pkCol = s.name; break; } }
|
|
390
|
-
loading = false;
|
|
391
|
-
editingCell = null;
|
|
392
|
-
renderGrid();
|
|
393
|
-
break;
|
|
394
|
-
case 'queryResult':
|
|
395
|
-
renderSqlResult(msg);
|
|
396
|
-
// Refresh grid data if it was a modify query
|
|
397
|
-
if (msg.changeType === 'modify') {
|
|
398
|
-
vscode.postMessage({ type: 'refresh' });
|
|
399
|
-
}
|
|
400
|
-
break;
|
|
401
|
-
case 'queryError':
|
|
402
|
-
renderSqlResult(msg);
|
|
403
|
-
break;
|
|
404
|
-
case 'cellUpdated':
|
|
405
|
-
// Refresh to show updated data
|
|
406
|
-
vscode.postMessage({ type: 'refresh' });
|
|
407
|
-
break;
|
|
408
|
-
case 'error':
|
|
409
|
-
loading = false;
|
|
410
|
-
gridScroll.innerHTML = '<div class="state-msg" style="color:var(--red)">' + escH(msg.message || 'Error') + '</div>';
|
|
411
|
-
break;
|
|
412
|
-
}
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
function escH(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
416
|
-
})();
|
|
417
|
-
</script>
|
|
418
|
-
</body>
|
|
419
|
-
</html>`;
|
|
420
|
-
}
|