admindb 1.0.1 → 1.0.3
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/LICENSE +20 -20
- package/README.md +333 -325
- package/dist/db/database.d.ts +3 -2
- package/dist/db/database.js +12 -9
- package/dist/db/database.js.map +1 -1
- package/dist/db/manager.js +1 -1
- package/dist/db/manager.js.map +1 -1
- package/dist/public/css/input.css +179 -179
- package/dist/public/js/api.js +47 -47
- package/dist/public/js/app.js +120 -120
- package/dist/public/js/browse.js +123 -123
- package/dist/public/js/databases.js +45 -45
- package/dist/public/js/designer.js +205 -205
- package/dist/public/js/forms.js +244 -244
- package/dist/public/js/query.js +232 -232
- package/dist/public/js/schema.js +314 -314
- package/dist/public/js/triggers.js +155 -155
- package/dist/public/js/views.js +176 -176
- package/dist/views/layouts/main.hbs +49 -49
- package/dist/views/pages/databases.hbs +102 -102
- package/dist/views/pages/designer.hbs +81 -81
- package/dist/views/pages/error.hbs +12 -12
- package/dist/views/pages/form.hbs +62 -62
- package/dist/views/pages/home.hbs +123 -123
- package/dist/views/pages/query.hbs +94 -94
- package/dist/views/pages/schema.hbs +290 -290
- package/dist/views/pages/table.hbs +203 -203
- package/dist/views/pages/triggers.hbs +162 -162
- package/dist/views/pages/views.hbs +134 -134
- package/dist/views/partials/navbar.hbs +50 -50
- package/dist/views/partials/sidebar.hbs +128 -128
- package/package.json +66 -62
package/dist/public/js/query.js
CHANGED
|
@@ -1,232 +1,232 @@
|
|
|
1
|
-
/* Query editor: run arbitrary SQL, save/load named queries, render results. */
|
|
2
|
-
(function () {
|
|
3
|
-
'use strict';
|
|
4
|
-
const editor = document.getElementById('query-sql');
|
|
5
|
-
const gutter = document.getElementById('query-gutter');
|
|
6
|
-
const runBtn = document.getElementById('run-query');
|
|
7
|
-
const messageEl = document.getElementById('query-message');
|
|
8
|
-
const resultEl = document.getElementById('query-result');
|
|
9
|
-
const headEl = document.getElementById('query-result-head');
|
|
10
|
-
const bodyEl = document.getElementById('query-result-body');
|
|
11
|
-
const resultSummary = document.getElementById('result-summary');
|
|
12
|
-
const saveBtns = Array.prototype.slice.call(document.querySelectorAll('#save-query, #save-query-inline'));
|
|
13
|
-
const saveName = document.getElementById('save-query-name');
|
|
14
|
-
const selectEl = document.getElementById('saved-query-select');
|
|
15
|
-
const deleteBtn = document.getElementById('delete-query');
|
|
16
|
-
const exportCsvBtn = document.getElementById('export-csv');
|
|
17
|
-
const exportJsonBtn = document.getElementById('export-json');
|
|
18
|
-
|
|
19
|
-
let queriesCache = [];
|
|
20
|
-
|
|
21
|
-
function setExportEnabled(enabled) {
|
|
22
|
-
if (exportCsvBtn) exportCsvBtn.disabled = !enabled;
|
|
23
|
-
if (exportJsonBtn) exportJsonBtn.disabled = !enabled;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ---- Export results ------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
async function exportResults(format) {
|
|
29
|
-
const sql = editor.value.trim();
|
|
30
|
-
if (!sql) return;
|
|
31
|
-
try {
|
|
32
|
-
const res = await fetch(Api.url('/api/query/export'), {
|
|
33
|
-
method: 'POST',
|
|
34
|
-
headers: { 'Content-Type': 'application/json' },
|
|
35
|
-
body: JSON.stringify({ sql, format }),
|
|
36
|
-
});
|
|
37
|
-
if (!res.ok) {
|
|
38
|
-
let msg = 'Export failed (' + res.status + ').';
|
|
39
|
-
try {
|
|
40
|
-
const data = await res.json();
|
|
41
|
-
if (data && data.error) msg = data.error;
|
|
42
|
-
} catch (e) { /* ignore */ }
|
|
43
|
-
throw new Error(msg);
|
|
44
|
-
}
|
|
45
|
-
const blob = await res.blob();
|
|
46
|
-
const url = URL.createObjectURL(blob);
|
|
47
|
-
const a = document.createElement('a');
|
|
48
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
49
|
-
a.href = url;
|
|
50
|
-
a.download = 'query-' + stamp + '.' + format;
|
|
51
|
-
document.body.appendChild(a);
|
|
52
|
-
a.click();
|
|
53
|
-
a.remove();
|
|
54
|
-
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
55
|
-
UI.showToast('Exported as ' + format.toUpperCase() + '.', 'success');
|
|
56
|
-
} catch (e) {
|
|
57
|
-
setMessage(e.message, 'error');
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (exportCsvBtn) exportCsvBtn.addEventListener('click', () => exportResults('csv'));
|
|
62
|
-
if (exportJsonBtn) exportJsonBtn.addEventListener('click', () => exportResults('json'));
|
|
63
|
-
|
|
64
|
-
function escapeHtml(s) {
|
|
65
|
-
return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function setMessage(text, type) {
|
|
69
|
-
if (!text) {
|
|
70
|
-
messageEl.innerHTML = '';
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
const palette = {
|
|
74
|
-
error: 'border-error/40 bg-error/10 text-error',
|
|
75
|
-
success: 'border-success/40 bg-success/10 text-success',
|
|
76
|
-
info: 'border-info/40 bg-info/10 text-info',
|
|
77
|
-
warning: 'border-warning/40 bg-warning/10 text-warning',
|
|
78
|
-
};
|
|
79
|
-
messageEl.innerHTML = '<div class="flex items-start gap-2.5 rounded-xl border px-4 py-3 text-sm font-medium ' +
|
|
80
|
-
(palette[type] || palette.info) + '"><span>' + escapeHtml(text) + '</span></div>';
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function renderTable(columns, rows) {
|
|
84
|
-
headEl.innerHTML = '<tr>' + columns.map((c) => '<th class="whitespace-nowrap font-semibold"><span class="font-mono normal-case">' + escapeHtml(c) + '</span></th>').join('') + '</tr>';
|
|
85
|
-
if (!rows.length) {
|
|
86
|
-
bodyEl.innerHTML = '<tr><td colspan="' + Math.max(1, columns.length) + '" class="py-10 text-center text-base-content/45">No rows returned.</td></tr>';
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
bodyEl.innerHTML = rows.map((row) =>
|
|
90
|
-
'<tr class="hover:bg-primary/[0.04]">' + columns.map((c) => {
|
|
91
|
-
const v = row[c];
|
|
92
|
-
if (v === null || v === undefined) {
|
|
93
|
-
return '<td class="align-middle"><span class="chip chip-null">NULL</span></td>';
|
|
94
|
-
}
|
|
95
|
-
const s = String(v);
|
|
96
|
-
const short = s.length > 200 ? s.slice(0, 200) + '…' : s;
|
|
97
|
-
return '<td class="max-w-xs truncate align-middle font-mono text-[13px]" title="' + escapeHtml(s) + '">' + escapeHtml(short) + '</td>';
|
|
98
|
-
}).join('') + '</tr>',
|
|
99
|
-
).join('');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function renderMessageRow(text, kindLabel) {
|
|
103
|
-
headEl.innerHTML = '';
|
|
104
|
-
bodyEl.innerHTML = '<tr><td class="px-4 py-8 text-center"><p class="text-2xl font-extrabold text-primary">' +
|
|
105
|
-
escapeHtml(text) + '</p><p class="mt-1 text-sm text-base-content/45">' + escapeHtml(kindLabel) + '</p></td></tr>';
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function setSummary(text) {
|
|
109
|
-
if (resultSummary) resultSummary.textContent = text;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function run() {
|
|
113
|
-
const sql = editor.value.trim();
|
|
114
|
-
if (!sql) {
|
|
115
|
-
setMessage('Enter a SQL statement first.', 'warning');
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
messageEl.innerHTML = '';
|
|
119
|
-
resultEl.classList.add('hidden');
|
|
120
|
-
setExportEnabled(false);
|
|
121
|
-
const started = performance.now();
|
|
122
|
-
try {
|
|
123
|
-
const data = await Api.post('/api/query', { sql });
|
|
124
|
-
const elapsed = Math.max(1, Math.round(performance.now() - started));
|
|
125
|
-
resultEl.classList.remove('hidden');
|
|
126
|
-
if (data.kind === 'count') {
|
|
127
|
-
renderMessageRow(String(data.count), 'count');
|
|
128
|
-
setSummary('Count query · ' + elapsed + 'ms');
|
|
129
|
-
} else if (data.kind === 'select') {
|
|
130
|
-
renderTable(data.columns, data.rows);
|
|
131
|
-
setSummary(data.rows.length + ' row(s) · ' + elapsed + 'ms');
|
|
132
|
-
setExportEnabled(true);
|
|
133
|
-
} else {
|
|
134
|
-
renderMessageRow(data.message || 'Statement executed.', 'write');
|
|
135
|
-
setSummary(data.changes != null ? data.changes + ' row(s) affected · ' + elapsed + 'ms' : elapsed + 'ms');
|
|
136
|
-
}
|
|
137
|
-
} catch (e) {
|
|
138
|
-
resultEl.classList.add('hidden');
|
|
139
|
-
setMessage(e.message, 'error');
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ---- Line numbers ------------------------------------------------------
|
|
144
|
-
|
|
145
|
-
function updateGutter() {
|
|
146
|
-
if (!gutter) return;
|
|
147
|
-
const lines = editor.value.split('\n').length;
|
|
148
|
-
let html = '';
|
|
149
|
-
for (let i = 1; i <= lines; i++) html += i + '\n';
|
|
150
|
-
gutter.textContent = html;
|
|
151
|
-
gutter.scrollTop = editor.scrollTop;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (editor && gutter) {
|
|
155
|
-
editor.addEventListener('scroll', () => {
|
|
156
|
-
gutter.scrollTop = editor.scrollTop;
|
|
157
|
-
});
|
|
158
|
-
editor.addEventListener('input', updateGutter);
|
|
159
|
-
editor.addEventListener('keydown', (e) => {
|
|
160
|
-
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
161
|
-
e.preventDefault();
|
|
162
|
-
run();
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
updateGutter();
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ---- Saved queries -----------------------------------------------------
|
|
169
|
-
|
|
170
|
-
async function refreshQueries(selectName) {
|
|
171
|
-
try {
|
|
172
|
-
queriesCache = await Api.get('/api/queries');
|
|
173
|
-
} catch (e) {
|
|
174
|
-
queriesCache = [];
|
|
175
|
-
}
|
|
176
|
-
selectEl.innerHTML = '<option value="">— select a saved query —</option>' + queriesCache.map((q) =>
|
|
177
|
-
'<option value="' + q.id + '"' + (selectName && q.name === selectName ? ' selected' : '') + '>' + escapeHtml(q.name) + '</option>',
|
|
178
|
-
).join('');
|
|
179
|
-
deleteBtn.disabled = true;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function saveQuery() {
|
|
183
|
-
const sql = editor.value.trim();
|
|
184
|
-
const name = saveName.value.trim();
|
|
185
|
-
if (!sql) {
|
|
186
|
-
setMessage('Write some SQL before saving.', 'warning');
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
if (!name) {
|
|
190
|
-
setMessage('Give the query a name.', 'warning');
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
try {
|
|
194
|
-
await Api.post('/api/queries', { name, sql });
|
|
195
|
-
UI.showToast('Query saved.', 'success');
|
|
196
|
-
await refreshQueries(name);
|
|
197
|
-
} catch (e) {
|
|
198
|
-
setMessage(e.message, 'error');
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async function deleteQuery() {
|
|
203
|
-
const id = selectEl.value;
|
|
204
|
-
if (!id) return;
|
|
205
|
-
if (!window.confirm('Delete this saved query?')) return;
|
|
206
|
-
try {
|
|
207
|
-
await Api.del('/api/queries/' + id);
|
|
208
|
-
UI.showToast('Query deleted.', 'success');
|
|
209
|
-
await refreshQueries();
|
|
210
|
-
} catch (e) {
|
|
211
|
-
setMessage(e.message, 'error');
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (runBtn) runBtn.addEventListener('click', run);
|
|
216
|
-
saveBtns.forEach((btn) => btn.addEventListener('click', saveQuery));
|
|
217
|
-
if (selectEl) {
|
|
218
|
-
selectEl.addEventListener('change', () => {
|
|
219
|
-
const id = selectEl.value;
|
|
220
|
-
deleteBtn.disabled = !id;
|
|
221
|
-
if (!id) return;
|
|
222
|
-
const q = queriesCache.find((x) => String(x.id) === String(id));
|
|
223
|
-
if (q) {
|
|
224
|
-
editor.value = q.sql;
|
|
225
|
-
saveName.value = q.name;
|
|
226
|
-
updateGutter();
|
|
227
|
-
setMessage('Loaded "' + q.name + '".', 'info');
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
if (deleteBtn) deleteBtn.addEventListener('click', deleteQuery);
|
|
232
|
-
})();
|
|
1
|
+
/* Query editor: run arbitrary SQL, save/load named queries, render results. */
|
|
2
|
+
(function () {
|
|
3
|
+
'use strict';
|
|
4
|
+
const editor = document.getElementById('query-sql');
|
|
5
|
+
const gutter = document.getElementById('query-gutter');
|
|
6
|
+
const runBtn = document.getElementById('run-query');
|
|
7
|
+
const messageEl = document.getElementById('query-message');
|
|
8
|
+
const resultEl = document.getElementById('query-result');
|
|
9
|
+
const headEl = document.getElementById('query-result-head');
|
|
10
|
+
const bodyEl = document.getElementById('query-result-body');
|
|
11
|
+
const resultSummary = document.getElementById('result-summary');
|
|
12
|
+
const saveBtns = Array.prototype.slice.call(document.querySelectorAll('#save-query, #save-query-inline'));
|
|
13
|
+
const saveName = document.getElementById('save-query-name');
|
|
14
|
+
const selectEl = document.getElementById('saved-query-select');
|
|
15
|
+
const deleteBtn = document.getElementById('delete-query');
|
|
16
|
+
const exportCsvBtn = document.getElementById('export-csv');
|
|
17
|
+
const exportJsonBtn = document.getElementById('export-json');
|
|
18
|
+
|
|
19
|
+
let queriesCache = [];
|
|
20
|
+
|
|
21
|
+
function setExportEnabled(enabled) {
|
|
22
|
+
if (exportCsvBtn) exportCsvBtn.disabled = !enabled;
|
|
23
|
+
if (exportJsonBtn) exportJsonBtn.disabled = !enabled;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ---- Export results ------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
async function exportResults(format) {
|
|
29
|
+
const sql = editor.value.trim();
|
|
30
|
+
if (!sql) return;
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetch(Api.url('/api/query/export'), {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
body: JSON.stringify({ sql, format }),
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
let msg = 'Export failed (' + res.status + ').';
|
|
39
|
+
try {
|
|
40
|
+
const data = await res.json();
|
|
41
|
+
if (data && data.error) msg = data.error;
|
|
42
|
+
} catch (e) { /* ignore */ }
|
|
43
|
+
throw new Error(msg);
|
|
44
|
+
}
|
|
45
|
+
const blob = await res.blob();
|
|
46
|
+
const url = URL.createObjectURL(blob);
|
|
47
|
+
const a = document.createElement('a');
|
|
48
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
49
|
+
a.href = url;
|
|
50
|
+
a.download = 'query-' + stamp + '.' + format;
|
|
51
|
+
document.body.appendChild(a);
|
|
52
|
+
a.click();
|
|
53
|
+
a.remove();
|
|
54
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
55
|
+
UI.showToast('Exported as ' + format.toUpperCase() + '.', 'success');
|
|
56
|
+
} catch (e) {
|
|
57
|
+
setMessage(e.message, 'error');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (exportCsvBtn) exportCsvBtn.addEventListener('click', () => exportResults('csv'));
|
|
62
|
+
if (exportJsonBtn) exportJsonBtn.addEventListener('click', () => exportResults('json'));
|
|
63
|
+
|
|
64
|
+
function escapeHtml(s) {
|
|
65
|
+
return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function setMessage(text, type) {
|
|
69
|
+
if (!text) {
|
|
70
|
+
messageEl.innerHTML = '';
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const palette = {
|
|
74
|
+
error: 'border-error/40 bg-error/10 text-error',
|
|
75
|
+
success: 'border-success/40 bg-success/10 text-success',
|
|
76
|
+
info: 'border-info/40 bg-info/10 text-info',
|
|
77
|
+
warning: 'border-warning/40 bg-warning/10 text-warning',
|
|
78
|
+
};
|
|
79
|
+
messageEl.innerHTML = '<div class="flex items-start gap-2.5 rounded-xl border px-4 py-3 text-sm font-medium ' +
|
|
80
|
+
(palette[type] || palette.info) + '"><span>' + escapeHtml(text) + '</span></div>';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function renderTable(columns, rows) {
|
|
84
|
+
headEl.innerHTML = '<tr>' + columns.map((c) => '<th class="whitespace-nowrap font-semibold"><span class="font-mono normal-case">' + escapeHtml(c) + '</span></th>').join('') + '</tr>';
|
|
85
|
+
if (!rows.length) {
|
|
86
|
+
bodyEl.innerHTML = '<tr><td colspan="' + Math.max(1, columns.length) + '" class="py-10 text-center text-base-content/45">No rows returned.</td></tr>';
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
bodyEl.innerHTML = rows.map((row) =>
|
|
90
|
+
'<tr class="hover:bg-primary/[0.04]">' + columns.map((c) => {
|
|
91
|
+
const v = row[c];
|
|
92
|
+
if (v === null || v === undefined) {
|
|
93
|
+
return '<td class="align-middle"><span class="chip chip-null">NULL</span></td>';
|
|
94
|
+
}
|
|
95
|
+
const s = String(v);
|
|
96
|
+
const short = s.length > 200 ? s.slice(0, 200) + '…' : s;
|
|
97
|
+
return '<td class="max-w-xs truncate align-middle font-mono text-[13px]" title="' + escapeHtml(s) + '">' + escapeHtml(short) + '</td>';
|
|
98
|
+
}).join('') + '</tr>',
|
|
99
|
+
).join('');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderMessageRow(text, kindLabel) {
|
|
103
|
+
headEl.innerHTML = '';
|
|
104
|
+
bodyEl.innerHTML = '<tr><td class="px-4 py-8 text-center"><p class="text-2xl font-extrabold text-primary">' +
|
|
105
|
+
escapeHtml(text) + '</p><p class="mt-1 text-sm text-base-content/45">' + escapeHtml(kindLabel) + '</p></td></tr>';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function setSummary(text) {
|
|
109
|
+
if (resultSummary) resultSummary.textContent = text;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function run() {
|
|
113
|
+
const sql = editor.value.trim();
|
|
114
|
+
if (!sql) {
|
|
115
|
+
setMessage('Enter a SQL statement first.', 'warning');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
messageEl.innerHTML = '';
|
|
119
|
+
resultEl.classList.add('hidden');
|
|
120
|
+
setExportEnabled(false);
|
|
121
|
+
const started = performance.now();
|
|
122
|
+
try {
|
|
123
|
+
const data = await Api.post('/api/query', { sql });
|
|
124
|
+
const elapsed = Math.max(1, Math.round(performance.now() - started));
|
|
125
|
+
resultEl.classList.remove('hidden');
|
|
126
|
+
if (data.kind === 'count') {
|
|
127
|
+
renderMessageRow(String(data.count), 'count');
|
|
128
|
+
setSummary('Count query · ' + elapsed + 'ms');
|
|
129
|
+
} else if (data.kind === 'select') {
|
|
130
|
+
renderTable(data.columns, data.rows);
|
|
131
|
+
setSummary(data.rows.length + ' row(s) · ' + elapsed + 'ms');
|
|
132
|
+
setExportEnabled(true);
|
|
133
|
+
} else {
|
|
134
|
+
renderMessageRow(data.message || 'Statement executed.', 'write');
|
|
135
|
+
setSummary(data.changes != null ? data.changes + ' row(s) affected · ' + elapsed + 'ms' : elapsed + 'ms');
|
|
136
|
+
}
|
|
137
|
+
} catch (e) {
|
|
138
|
+
resultEl.classList.add('hidden');
|
|
139
|
+
setMessage(e.message, 'error');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---- Line numbers ------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
function updateGutter() {
|
|
146
|
+
if (!gutter) return;
|
|
147
|
+
const lines = editor.value.split('\n').length;
|
|
148
|
+
let html = '';
|
|
149
|
+
for (let i = 1; i <= lines; i++) html += i + '\n';
|
|
150
|
+
gutter.textContent = html;
|
|
151
|
+
gutter.scrollTop = editor.scrollTop;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (editor && gutter) {
|
|
155
|
+
editor.addEventListener('scroll', () => {
|
|
156
|
+
gutter.scrollTop = editor.scrollTop;
|
|
157
|
+
});
|
|
158
|
+
editor.addEventListener('input', updateGutter);
|
|
159
|
+
editor.addEventListener('keydown', (e) => {
|
|
160
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
run();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
updateGutter();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---- Saved queries -----------------------------------------------------
|
|
169
|
+
|
|
170
|
+
async function refreshQueries(selectName) {
|
|
171
|
+
try {
|
|
172
|
+
queriesCache = await Api.get('/api/queries');
|
|
173
|
+
} catch (e) {
|
|
174
|
+
queriesCache = [];
|
|
175
|
+
}
|
|
176
|
+
selectEl.innerHTML = '<option value="">— select a saved query —</option>' + queriesCache.map((q) =>
|
|
177
|
+
'<option value="' + q.id + '"' + (selectName && q.name === selectName ? ' selected' : '') + '>' + escapeHtml(q.name) + '</option>',
|
|
178
|
+
).join('');
|
|
179
|
+
deleteBtn.disabled = true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function saveQuery() {
|
|
183
|
+
const sql = editor.value.trim();
|
|
184
|
+
const name = saveName.value.trim();
|
|
185
|
+
if (!sql) {
|
|
186
|
+
setMessage('Write some SQL before saving.', 'warning');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (!name) {
|
|
190
|
+
setMessage('Give the query a name.', 'warning');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
await Api.post('/api/queries', { name, sql });
|
|
195
|
+
UI.showToast('Query saved.', 'success');
|
|
196
|
+
await refreshQueries(name);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
setMessage(e.message, 'error');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function deleteQuery() {
|
|
203
|
+
const id = selectEl.value;
|
|
204
|
+
if (!id) return;
|
|
205
|
+
if (!window.confirm('Delete this saved query?')) return;
|
|
206
|
+
try {
|
|
207
|
+
await Api.del('/api/queries/' + id);
|
|
208
|
+
UI.showToast('Query deleted.', 'success');
|
|
209
|
+
await refreshQueries();
|
|
210
|
+
} catch (e) {
|
|
211
|
+
setMessage(e.message, 'error');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (runBtn) runBtn.addEventListener('click', run);
|
|
216
|
+
saveBtns.forEach((btn) => btn.addEventListener('click', saveQuery));
|
|
217
|
+
if (selectEl) {
|
|
218
|
+
selectEl.addEventListener('change', () => {
|
|
219
|
+
const id = selectEl.value;
|
|
220
|
+
deleteBtn.disabled = !id;
|
|
221
|
+
if (!id) return;
|
|
222
|
+
const q = queriesCache.find((x) => String(x.id) === String(id));
|
|
223
|
+
if (q) {
|
|
224
|
+
editor.value = q.sql;
|
|
225
|
+
saveName.value = q.name;
|
|
226
|
+
updateGutter();
|
|
227
|
+
setMessage('Loaded "' + q.name + '".', 'info');
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (deleteBtn) deleteBtn.addEventListener('click', deleteQuery);
|
|
232
|
+
})();
|