admindb 1.0.3 → 1.1.0
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/README.md +51 -30
- package/dist/db/database.d.ts +58 -37
- package/dist/db/database.js +239 -42
- package/dist/db/database.js.map +1 -1
- package/dist/db/manager.d.ts +2 -2
- package/dist/db/manager.js +2 -1
- package/dist/db/manager.js.map +1 -1
- package/dist/public/css/app.css +1 -1
- package/dist/public/css/input.css +53 -0
- package/dist/public/js/browse.js +840 -86
- package/dist/routes/api.js +261 -97
- package/dist/routes/api.js.map +1 -1
- package/dist/routes/pages.js +50 -30
- package/dist/routes/pages.js.map +1 -1
- package/dist/sql/generator.d.ts +0 -22
- package/dist/sql/generator.js +0 -48
- package/dist/sql/generator.js.map +1 -1
- package/dist/test/database.test.js +241 -63
- package/dist/test/database.test.js.map +1 -1
- package/dist/test/generator.test.js +0 -32
- package/dist/test/generator.test.js.map +1 -1
- package/dist/util.d.ts +13 -1
- package/dist/util.js +24 -2
- package/dist/util.js.map +1 -1
- package/dist/views/pages/table.hbs +81 -28
- package/dist/views/partials/sidebar.hbs +0 -12
- package/package.json +1 -1
- package/dist/public/js/triggers.js +0 -155
- package/dist/public/js/views.js +0 -176
- package/dist/views/pages/triggers.hbs +0 -162
- package/dist/views/pages/views.hbs +0 -134
|
@@ -52,34 +52,31 @@
|
|
|
52
52
|
</div>
|
|
53
53
|
</div>
|
|
54
54
|
|
|
55
|
-
<!-- Row filters -->
|
|
56
|
-
<
|
|
57
|
-
<
|
|
55
|
+
<!-- Row filters (collapsed by default; auto-expands when filters are active) -->
|
|
56
|
+
<details class="app-card overflow-hidden" {{#if filterQS}}open{{/if}}>
|
|
57
|
+
<summary class="group flex cursor-pointer list-none items-center justify-between gap-2 px-4 py-3 transition hover:bg-base-200/40 [&::-webkit-details-marker]:hidden">
|
|
58
58
|
<span class="flex items-center gap-2 text-sm font-semibold uppercase tracking-wide text-base-content/45">
|
|
59
59
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414A1 1 0 0014 15.414V20a1 1 0 01-1 1h-2a1 1 0 01-1-1v-4.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" /></svg>
|
|
60
60
|
Filter rows
|
|
61
|
+
{{#if filterQS}}<span class="chip chip-pk">active</span>{{/if}}
|
|
61
62
|
</span>
|
|
62
|
-
<
|
|
63
|
-
</
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
</
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
<span class="text-xs text-base-content/50">Filtering <b class="tabular-nums">{{count}}</b> row(s).</span>
|
|
80
|
-
{{/if}}
|
|
81
|
-
</div>
|
|
82
|
-
</form>
|
|
63
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0 text-base-content/40 transition-transform group-open:rotate-180" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
|
|
64
|
+
</summary>
|
|
65
|
+
<form id="filter-form" class="border-t border-base-200 p-4">
|
|
66
|
+
<p class="mb-3 text-xs text-base-content/40">Filters adapt to each column's type: foreign keys, booleans, dates and numbers get dedicated controls. Text columns keep the operator syntax — <code>=exact</code> · <code>>5</code> · <code>pre*</code> starts with · plain text = contains.</p>
|
|
67
|
+
<div id="filter-fields" class="grid grid-cols-2 gap-2 md:grid-cols-3 xl:grid-cols-4"></div>
|
|
68
|
+
<div class="mt-3 flex flex-wrap items-center gap-2">
|
|
69
|
+
<button type="submit" class="btn btn-primary btn-sm gap-1.5">
|
|
70
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
|
71
|
+
Apply filter
|
|
72
|
+
</button>
|
|
73
|
+
<button type="button" id="clear-filter" class="btn btn-ghost btn-sm">Clear</button>
|
|
74
|
+
{{#if filterQS}}
|
|
75
|
+
<span class="text-xs text-base-content/50">Filtering <b class="tabular-nums">{{count}}</b> row(s).</span>
|
|
76
|
+
{{/if}}
|
|
77
|
+
</div>
|
|
78
|
+
</form>
|
|
79
|
+
</details>
|
|
83
80
|
|
|
84
81
|
<!-- CSV import -->
|
|
85
82
|
<div id="import-panel" class="app-card hidden p-4">
|
|
@@ -113,10 +110,37 @@
|
|
|
113
110
|
</label>
|
|
114
111
|
</div>
|
|
115
112
|
|
|
113
|
+
<!-- Bulk selection bar -->
|
|
114
|
+
<div id="bulk-bar" class="hidden items-center justify-between gap-2 border-b border-base-200 bg-primary/[0.04] px-4 py-2.5">
|
|
115
|
+
<span class="text-sm font-medium text-base-content/80">
|
|
116
|
+
<b id="bulk-count" class="tabular-nums text-primary">0</b> row(s) selected
|
|
117
|
+
</span>
|
|
118
|
+
<div class="flex flex-wrap items-center gap-1.5">
|
|
119
|
+
<button id="bulk-export-csv" type="button" class="btn btn-ghost btn-xs gap-1 text-base-content/70 hover:text-primary">
|
|
120
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
|
|
121
|
+
Export CSV
|
|
122
|
+
</button>
|
|
123
|
+
<button id="bulk-export-json" type="button" class="btn btn-ghost btn-xs gap-1 text-base-content/70 hover:text-primary">
|
|
124
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
|
|
125
|
+
Export JSON
|
|
126
|
+
</button>
|
|
127
|
+
<button id="bulk-delete" type="button" class="btn btn-error btn-xs gap-1" {{#if readonly}}disabled{{/if}}>
|
|
128
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
|
129
|
+
Delete
|
|
130
|
+
</button>
|
|
131
|
+
<button id="bulk-clear" type="button" class="btn btn-ghost btn-xs text-base-content/60">Clear</button>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
116
135
|
<div class="overflow-x-auto">
|
|
117
136
|
<table class="table table-sm table-pin-rows">
|
|
118
137
|
<thead>
|
|
119
138
|
<tr class="bg-base-200/70 text-xs uppercase tracking-wide text-base-content/50">
|
|
139
|
+
{{#if hasPk}}
|
|
140
|
+
<th class="w-10 text-center font-semibold">
|
|
141
|
+
<input type="checkbox" id="select-all-rows" class="checkbox checkbox-primary checkbox-xs" title="Select all rows on this page" />
|
|
142
|
+
</th>
|
|
143
|
+
{{/if}}
|
|
120
144
|
{{#each colHeaders}}
|
|
121
145
|
<th class="whitespace-nowrap font-semibold">
|
|
122
146
|
<a href="{{this.href}}" class="sort-link {{#if this.active}}active{{/if}}" title="Sort by {{this.name}}">
|
|
@@ -127,17 +151,30 @@
|
|
|
127
151
|
</th>
|
|
128
152
|
{{/each}}
|
|
129
153
|
<th class="text-right font-semibold">Actions</th>
|
|
154
|
+
{{#each refColumns}}
|
|
155
|
+
<th class="whitespace-nowrap text-right font-semibold" title="Rows in {{this.table}} where {{this.from}} references {{this.to}}">
|
|
156
|
+
<span class="font-mono normal-case">{{this.table}}</span>
|
|
157
|
+
</th>
|
|
158
|
+
{{/each}}
|
|
130
159
|
</tr>
|
|
131
160
|
</thead>
|
|
132
161
|
<tbody>
|
|
133
162
|
{{#each rows}}
|
|
134
|
-
<tr class="group hover:bg-primary/[0.04]">
|
|
163
|
+
<tr class="group hover:bg-primary/[0.04]" data-table="{{../table}}" data-pk="{{this.pkEncoded}}">
|
|
164
|
+
{{#if ../hasPk}}
|
|
165
|
+
<td class="w-10 text-center align-middle">
|
|
166
|
+
{{#if this.pkEncoded}}
|
|
167
|
+
<input type="checkbox" class="row-checkbox checkbox checkbox-primary checkbox-xs" value="{{this.pkEncoded}}" aria-label="Select this row" />
|
|
168
|
+
{{/if}}
|
|
169
|
+
</td>
|
|
170
|
+
{{/if}}
|
|
135
171
|
{{#each this.cells}}
|
|
136
|
-
<td class="max-w-xs align-middle"
|
|
172
|
+
<td class="max-w-xs align-middle cell-editable {{#unless ../../readonly}}js-inline-cell{{/unless}}"
|
|
173
|
+
data-col="{{this.name}}" data-null="{{this.isNull}}" data-value="{{this.display}}">
|
|
137
174
|
{{#if this.isNull}}
|
|
138
|
-
<span class="chip chip-null">NULL</span>
|
|
175
|
+
<span class="chip chip-null js-cell-value">NULL</span>
|
|
139
176
|
{{else}}
|
|
140
|
-
<span class="block max-w-xs truncate font-mono text-[13px]" title="{{this.display}}">{{truncate this.display 120}}</span>
|
|
177
|
+
<span class="js-cell-value block max-w-xs truncate font-mono text-[13px]" title="{{this.display}}">{{truncate this.display 120}}</span>
|
|
141
178
|
{{/if}}
|
|
142
179
|
</td>
|
|
143
180
|
{{/each}}
|
|
@@ -160,6 +197,21 @@
|
|
|
160
197
|
{{/if}}
|
|
161
198
|
{{/unless}}
|
|
162
199
|
</td>
|
|
200
|
+
{{#each this.refs}}
|
|
201
|
+
<td class="whitespace-nowrap text-right align-middle">
|
|
202
|
+
{{#if this.value}}
|
|
203
|
+
<button type="button" class="js-ref-toggle inline-flex items-center gap-1.5 rounded-lg border border-base-300 bg-base-100 px-2 py-1 text-xs font-medium text-base-content/70 shadow-soft transition hover:border-primary/50 hover:text-primary {{#unless this.count}}opacity-50{{/unless}}"
|
|
204
|
+
data-table="{{../../table}}" data-pk="{{../pkEncoded}}" data-ref-table="{{this.table}}"
|
|
205
|
+
aria-expanded="false" title="View {{this.table}} rows where {{this.from}} = {{this.value}}">
|
|
206
|
+
<span class="font-mono normal-case">{{this.table}}</span>
|
|
207
|
+
<span class="rounded-md bg-primary/10 px-1.5 py-0.5 font-semibold tabular-nums text-primary">{{this.count}}</span>
|
|
208
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="js-ref-chevron h-3 w-3 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
|
|
209
|
+
</button>
|
|
210
|
+
{{else}}
|
|
211
|
+
<span class="text-xs text-base-content/35">—</span>
|
|
212
|
+
{{/if}}
|
|
213
|
+
</td>
|
|
214
|
+
{{/each}}
|
|
163
215
|
</tr>
|
|
164
216
|
{{/each}}
|
|
165
217
|
</tbody>
|
|
@@ -200,4 +252,5 @@
|
|
|
200
252
|
</div>
|
|
201
253
|
{{/if}}
|
|
202
254
|
</div>
|
|
255
|
+
<script type="application/json" id="browse-config">{{{json browseConfig}}}</script>
|
|
203
256
|
<script src="{{basePath}}/js/browse.js" defer></script>
|
|
@@ -55,18 +55,6 @@
|
|
|
55
55
|
Query editor
|
|
56
56
|
</a>
|
|
57
57
|
</li>
|
|
58
|
-
<li>
|
|
59
|
-
<a href="{{basePath}}/views" class="side-link {{#if (startsWith currentPath '/views')}}active{{/if}}">
|
|
60
|
-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /></svg>
|
|
61
|
-
Views
|
|
62
|
-
</a>
|
|
63
|
-
</li>
|
|
64
|
-
<li>
|
|
65
|
-
<a href="{{basePath}}/triggers" class="side-link {{#if (startsWith currentPath '/triggers')}}active{{/if}}">
|
|
66
|
-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
|
67
|
-
Triggers
|
|
68
|
-
</a>
|
|
69
|
-
</li>
|
|
70
58
|
{{#unless readonly}}
|
|
71
59
|
<li>
|
|
72
60
|
<a href="{{basePath}}/designer" class="side-link {{#if (startsWith currentPath '/designer')}}active{{/if}}">
|
package/package.json
CHANGED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/* Triggers page: create / drop triggers with a live SQL preview. */
|
|
2
|
-
(function () {
|
|
3
|
-
'use strict';
|
|
4
|
-
const cfgEl = document.getElementById('triggers-config');
|
|
5
|
-
if (!cfgEl) return;
|
|
6
|
-
|
|
7
|
-
function escapeHtml(s) {
|
|
8
|
-
return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function showError(el, msg) {
|
|
12
|
-
el.classList.remove('hidden');
|
|
13
|
-
el.innerHTML = '<div class="flex items-start gap-2.5 rounded-xl border border-error/40 bg-error/10 px-4 py-3 text-sm font-medium text-error"><span>' + escapeHtml(msg) + '</span></div>';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// ---- Editor gutter ------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
const editor = document.getElementById('trigger-body');
|
|
19
|
-
const gutter = document.getElementById('trigger-gutter');
|
|
20
|
-
function updateGutter() {
|
|
21
|
-
if (!gutter || !editor) return;
|
|
22
|
-
const lines = editor.value.split('\n').length;
|
|
23
|
-
let html = '';
|
|
24
|
-
for (let i = 1; i <= lines; i++) html += i + '\n';
|
|
25
|
-
gutter.textContent = html;
|
|
26
|
-
gutter.scrollTop = editor.scrollTop;
|
|
27
|
-
}
|
|
28
|
-
if (editor && gutter) {
|
|
29
|
-
editor.addEventListener('scroll', () => { gutter.scrollTop = editor.scrollTop; });
|
|
30
|
-
editor.addEventListener('input', updateGutter);
|
|
31
|
-
updateGutter();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ---- New trigger form ---------------------------------------------------
|
|
35
|
-
|
|
36
|
-
const newBtn = document.getElementById('new-trigger');
|
|
37
|
-
const panel = document.getElementById('new-trigger-panel');
|
|
38
|
-
const nameInput = document.getElementById('trigger-name');
|
|
39
|
-
const tableInput = document.getElementById('trigger-table');
|
|
40
|
-
const timingInput = document.getElementById('trigger-timing');
|
|
41
|
-
const eventInput = document.getElementById('trigger-event');
|
|
42
|
-
const whenInput = document.getElementById('trigger-when');
|
|
43
|
-
const bodyInput = document.getElementById('trigger-body');
|
|
44
|
-
const errEl = document.getElementById('trigger-error');
|
|
45
|
-
const createBtn = document.getElementById('create-trigger');
|
|
46
|
-
const cancelBtn = document.getElementById('cancel-trigger');
|
|
47
|
-
const preview = document.getElementById('trigger-preview');
|
|
48
|
-
const previewCode = document.getElementById('trigger-preview-code');
|
|
49
|
-
const previewHint = document.getElementById('trigger-preview-hint');
|
|
50
|
-
let previewTimer = null;
|
|
51
|
-
|
|
52
|
-
function currentEvent() {
|
|
53
|
-
return eventInput ? eventInput.value : 'INSERT';
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function renderPreview() {
|
|
57
|
-
clearTimeout(previewTimer);
|
|
58
|
-
const name = (nameInput.value || '').trim();
|
|
59
|
-
const table = tableInput.value || '';
|
|
60
|
-
const event = currentEvent();
|
|
61
|
-
if (!name && !table && !(bodyInput.value || '').trim()) {
|
|
62
|
-
preview.classList.add('hidden');
|
|
63
|
-
previewCode.textContent = '';
|
|
64
|
-
if (previewHint) previewHint.classList.remove('hidden');
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
previewTimer = setTimeout(async () => {
|
|
68
|
-
try {
|
|
69
|
-
const data = await Api.post('/api/triggers/generate', {
|
|
70
|
-
name,
|
|
71
|
-
table,
|
|
72
|
-
timing: timingInput.value,
|
|
73
|
-
event,
|
|
74
|
-
when: (whenInput.value || '').trim() || undefined,
|
|
75
|
-
body: bodyInput.value,
|
|
76
|
-
});
|
|
77
|
-
previewCode.textContent = data.sql;
|
|
78
|
-
preview.classList.remove('hidden');
|
|
79
|
-
if (previewHint) previewHint.classList.add('hidden');
|
|
80
|
-
} catch (e) {
|
|
81
|
-
preview.classList.add('hidden');
|
|
82
|
-
previewCode.textContent = '';
|
|
83
|
-
if (previewHint) previewHint.classList.remove('hidden');
|
|
84
|
-
}
|
|
85
|
-
}, 250);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (newBtn && panel) {
|
|
89
|
-
newBtn.addEventListener('click', () => {
|
|
90
|
-
panel.classList.toggle('hidden');
|
|
91
|
-
if (!panel.classList.contains('hidden')) {
|
|
92
|
-
errEl.classList.add('hidden');
|
|
93
|
-
nameInput.focus();
|
|
94
|
-
renderPreview();
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
[nameInput, tableInput, timingInput, eventInput, whenInput, bodyInput].forEach((el) => {
|
|
99
|
-
if (el) el.addEventListener('input', renderPreview);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
if (cancelBtn) {
|
|
103
|
-
cancelBtn.addEventListener('click', () => {
|
|
104
|
-
panel.classList.add('hidden');
|
|
105
|
-
errEl.classList.add('hidden');
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (createBtn) {
|
|
110
|
-
createBtn.addEventListener('click', async () => {
|
|
111
|
-
errEl.classList.add('hidden');
|
|
112
|
-
const name = (nameInput.value || '').trim();
|
|
113
|
-
const table = tableInput.value || '';
|
|
114
|
-
const body = (bodyInput.value || '').trim();
|
|
115
|
-
const event = currentEvent();
|
|
116
|
-
if (!name) return showError(errEl, 'Trigger name is required.');
|
|
117
|
-
if (!table) return showError(errEl, 'Select a table for the trigger.');
|
|
118
|
-
if (!body) return showError(errEl, 'Trigger body is required.');
|
|
119
|
-
createBtn.disabled = true;
|
|
120
|
-
try {
|
|
121
|
-
const data = await Api.post('/api/triggers', {
|
|
122
|
-
name,
|
|
123
|
-
table,
|
|
124
|
-
timing: timingInput.value,
|
|
125
|
-
event,
|
|
126
|
-
when: (whenInput.value || '').trim() || undefined,
|
|
127
|
-
body,
|
|
128
|
-
});
|
|
129
|
-
UI.showToast(data.message || 'Trigger created.', 'success');
|
|
130
|
-
window.location.reload();
|
|
131
|
-
} catch (e) {
|
|
132
|
-
showError(errEl, e.message);
|
|
133
|
-
} finally {
|
|
134
|
-
createBtn.disabled = false;
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ---- Drop trigger -------------------------------------------------------
|
|
140
|
-
|
|
141
|
-
document.querySelectorAll('.trigger-drop').forEach((btn) => {
|
|
142
|
-
btn.addEventListener('click', async () => {
|
|
143
|
-
const name = btn.dataset.trigger;
|
|
144
|
-
if (!name) return;
|
|
145
|
-
if (!window.confirm('Drop trigger "' + name + '" permanently?')) return;
|
|
146
|
-
try {
|
|
147
|
-
const data = await Api.del('/api/triggers/' + encodeURIComponent(name));
|
|
148
|
-
UI.showToast(data.message || 'Trigger dropped.', 'success');
|
|
149
|
-
window.location.reload();
|
|
150
|
-
} catch (e) {
|
|
151
|
-
UI.showError(e.message);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
})();
|
package/dist/public/js/views.js
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
/* Views page: create / drop views, preview rows. */
|
|
2
|
-
(function () {
|
|
3
|
-
'use strict';
|
|
4
|
-
const cfgEl = document.getElementById('views-config');
|
|
5
|
-
if (!cfgEl) return;
|
|
6
|
-
|
|
7
|
-
function escapeHtml(s) {
|
|
8
|
-
return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function showError(el, msg) {
|
|
12
|
-
el.classList.remove('hidden');
|
|
13
|
-
el.innerHTML = '<div class="flex items-start gap-2.5 rounded-xl border border-error/40 bg-error/10 px-4 py-3 text-sm font-medium text-error"><span>' + escapeHtml(msg) + '</span></div>';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// ---- Editor gutter ------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
const editor = document.getElementById('view-sql');
|
|
19
|
-
const gutter = document.getElementById('view-gutter');
|
|
20
|
-
function updateGutter() {
|
|
21
|
-
if (!gutter || !editor) return;
|
|
22
|
-
const lines = editor.value.split('\n').length;
|
|
23
|
-
let html = '';
|
|
24
|
-
for (let i = 1; i <= lines; i++) html += i + '\n';
|
|
25
|
-
gutter.textContent = html;
|
|
26
|
-
gutter.scrollTop = editor.scrollTop;
|
|
27
|
-
}
|
|
28
|
-
if (editor && gutter) {
|
|
29
|
-
editor.addEventListener('scroll', () => { gutter.scrollTop = editor.scrollTop; });
|
|
30
|
-
editor.addEventListener('input', updateGutter);
|
|
31
|
-
updateGutter();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ---- New view form ------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
const newBtn = document.getElementById('new-view');
|
|
37
|
-
const panel = document.getElementById('new-view-panel');
|
|
38
|
-
const nameInput = document.getElementById('view-name');
|
|
39
|
-
const sqlInput = document.getElementById('view-sql');
|
|
40
|
-
const errEl = document.getElementById('view-error');
|
|
41
|
-
const createBtn = document.getElementById('create-view');
|
|
42
|
-
const cancelBtn = document.getElementById('cancel-view');
|
|
43
|
-
const preview = document.getElementById('view-preview');
|
|
44
|
-
const previewCode = document.getElementById('view-preview-code');
|
|
45
|
-
const previewHint = document.getElementById('view-preview-hint');
|
|
46
|
-
let previewTimer = null;
|
|
47
|
-
|
|
48
|
-
function renderPreview() {
|
|
49
|
-
clearTimeout(previewTimer);
|
|
50
|
-
const name = (nameInput.value || '').trim();
|
|
51
|
-
const sql = (sqlInput.value || '').trim();
|
|
52
|
-
if (!name && !sql) {
|
|
53
|
-
preview.classList.add('hidden');
|
|
54
|
-
previewCode.textContent = '';
|
|
55
|
-
if (previewHint) previewHint.classList.remove('hidden');
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
previewTimer = setTimeout(async () => {
|
|
59
|
-
try {
|
|
60
|
-
const data = await Api.post('/api/views/generate', { name, sql });
|
|
61
|
-
previewCode.textContent = data.sql;
|
|
62
|
-
preview.classList.remove('hidden');
|
|
63
|
-
if (previewHint) previewHint.classList.add('hidden');
|
|
64
|
-
} catch (e) {
|
|
65
|
-
preview.classList.add('hidden');
|
|
66
|
-
previewCode.textContent = '';
|
|
67
|
-
if (previewHint) previewHint.classList.remove('hidden');
|
|
68
|
-
}
|
|
69
|
-
}, 250);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (newBtn && panel) {
|
|
73
|
-
newBtn.addEventListener('click', () => {
|
|
74
|
-
panel.classList.toggle('hidden');
|
|
75
|
-
if (!panel.classList.contains('hidden')) {
|
|
76
|
-
errEl.classList.add('hidden');
|
|
77
|
-
nameInput.focus();
|
|
78
|
-
renderPreview();
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
if (nameInput) nameInput.addEventListener('input', renderPreview);
|
|
83
|
-
if (sqlInput) sqlInput.addEventListener('input', renderPreview);
|
|
84
|
-
|
|
85
|
-
if (cancelBtn) {
|
|
86
|
-
cancelBtn.addEventListener('click', () => {
|
|
87
|
-
panel.classList.add('hidden');
|
|
88
|
-
errEl.classList.add('hidden');
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (createBtn) {
|
|
93
|
-
createBtn.addEventListener('click', async () => {
|
|
94
|
-
errEl.classList.add('hidden');
|
|
95
|
-
const name = (nameInput.value || '').trim();
|
|
96
|
-
const sql = (sqlInput.value || '').trim();
|
|
97
|
-
if (!name) return showError(errEl, 'View name is required.');
|
|
98
|
-
if (!sql) return showError(errEl, 'A SELECT statement is required.');
|
|
99
|
-
createBtn.disabled = true;
|
|
100
|
-
try {
|
|
101
|
-
const data = await Api.post('/api/views', { name, sql });
|
|
102
|
-
UI.showToast(data.message || 'View created.', 'success');
|
|
103
|
-
window.location.reload();
|
|
104
|
-
} catch (e) {
|
|
105
|
-
showError(errEl, e.message);
|
|
106
|
-
} finally {
|
|
107
|
-
createBtn.disabled = false;
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ---- Preview rows -------------------------------------------------------
|
|
113
|
-
|
|
114
|
-
document.querySelectorAll('.view-browse').forEach((btn) => {
|
|
115
|
-
btn.addEventListener('click', async () => {
|
|
116
|
-
const name = btn.dataset.view;
|
|
117
|
-
const tr = btn.closest('tr');
|
|
118
|
-
const previewTr = tr && tr.nextElementSibling;
|
|
119
|
-
if (!previewTr) return;
|
|
120
|
-
const container = previewTr.querySelector('td > div');
|
|
121
|
-
if (!container) return;
|
|
122
|
-
if (!previewTr.classList.contains('hidden')) {
|
|
123
|
-
previewTr.classList.add('hidden');
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
container.innerHTML = '<p class="text-sm text-base-content/45">Loading…</p>';
|
|
127
|
-
try {
|
|
128
|
-
const data = await Api.get('/api/views/' + encodeURIComponent(name) + '/rows');
|
|
129
|
-
const rows = data.rows || [];
|
|
130
|
-
if (!rows.length) {
|
|
131
|
-
container.innerHTML = '<p class="text-sm text-base-content/45">View returned no rows.</p>';
|
|
132
|
-
previewTr.classList.remove('hidden');
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
const cols = Object.keys(rows[0]);
|
|
136
|
-
let html = '<div class="overflow-x-auto rounded-lg border border-base-200"><table class="table table-xs">' +
|
|
137
|
-
'<thead><tr class="bg-base-200/70">' + cols.map((c) => '<th class="font-mono text-[11px]">' + escapeHtml(c) + '</th>').join('') + '</tr></thead><tbody>';
|
|
138
|
-
const shown = rows.slice(0, 50);
|
|
139
|
-
for (const row of shown) {
|
|
140
|
-
html += '<tr>' + cols.map((c) => {
|
|
141
|
-
const v = row[c];
|
|
142
|
-
const s = v == null ? '' : String(v);
|
|
143
|
-
const display = v == null ? '<span class="text-base-content/30">NULL</span>' : (s.length > 120 ? escapeHtml(s.slice(0, 120)) + '…' : escapeHtml(s));
|
|
144
|
-
return '<td class="max-w-xs truncate font-mono text-[12px]">' + display + '</td>';
|
|
145
|
-
}).join('') + '</tr>';
|
|
146
|
-
}
|
|
147
|
-
html += '</tbody></table></div>';
|
|
148
|
-
if (rows.length > shown.length) {
|
|
149
|
-
html += '<p class="mt-2 text-xs text-base-content/45">Showing first ' + shown.length + ' of ' + rows.length + ' row(s).</p>';
|
|
150
|
-
}
|
|
151
|
-
container.innerHTML = html;
|
|
152
|
-
previewTr.classList.remove('hidden');
|
|
153
|
-
} catch (e) {
|
|
154
|
-
container.innerHTML = '<p class="text-sm font-medium text-error">' + escapeHtml(e.message) + '</p>';
|
|
155
|
-
previewTr.classList.remove('hidden');
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// ---- Drop view ----------------------------------------------------------
|
|
161
|
-
|
|
162
|
-
document.querySelectorAll('.view-drop').forEach((btn) => {
|
|
163
|
-
btn.addEventListener('click', async () => {
|
|
164
|
-
const name = btn.dataset.view;
|
|
165
|
-
if (!name) return;
|
|
166
|
-
if (!window.confirm('Drop view "' + name + '" permanently?')) return;
|
|
167
|
-
try {
|
|
168
|
-
const data = await Api.del('/api/views/' + encodeURIComponent(name));
|
|
169
|
-
UI.showToast(data.message || 'View dropped.', 'success');
|
|
170
|
-
window.location.reload();
|
|
171
|
-
} catch (e) {
|
|
172
|
-
UI.showError(e.message);
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
})();
|