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.
@@ -1,176 +1,176 @@
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
- })();
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
+ })();
@@ -1,49 +1,49 @@
1
- <!DOCTYPE html>
2
- <html lang="en" data-theme="admindb">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>{{title}} · AdminDB</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com" />
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
10
- <link rel="stylesheet" href="{{basePath}}/css/app.css" />
11
- <script>
12
- window.APP = { basePath: {{{json basePath}}}, readonly: {{{json readonly}}} };
13
- // Apply the saved theme before first paint to avoid a flash.
14
- (function () {
15
- try {
16
- var t = localStorage.getItem('admindb-theme');
17
- if (t === 'dark') document.documentElement.setAttribute('data-theme', 'admindbDark');
18
- } catch (e) {}
19
- })();
20
- </script>
21
- <script src="{{basePath}}/js/api.js" defer></script>
22
- <script src="{{basePath}}/js/app.js" defer></script>
23
- </head>
24
- <body class="min-h-screen bg-base-200 text-base-content">
25
- {{#if readonly}}
26
- <div class="flex items-center justify-center gap-2 bg-warning/15 px-4 py-2 text-center text-sm font-semibold text-warning">
27
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
28
- Read-only mode — this database is open without write access. All writes are disabled.
29
- </div>
30
- {{/if}}
31
- <div class="drawer lg:drawer-open">
32
- <input id="sidebar-toggle" type="checkbox" class="drawer-toggle" />
33
- <div class="drawer-content flex min-h-screen flex-col">
34
- {{> navbar}}
35
- <main class="mx-auto w-full max-w-[1500px] flex-1 px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
36
- {{{body}}}
37
- </main>
38
- <footer class="border-t border-base-300 px-6 py-4 text-center text-xs text-base-content/40">
39
- AdminDB · a browser tool for SQLite databases
40
- </footer>
41
- </div>
42
- <div class="drawer-side z-40">
43
- <label for="sidebar-toggle" aria-label="close sidebar" class="drawer-overlay"></label>
44
- {{> sidebar}}
45
- </div>
46
- </div>
47
- <div id="toast-container" class="pointer-events-none fixed inset-x-0 top-4 z-50 flex flex-col items-center gap-2 px-4"></div>
48
- </body>
49
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="admindb">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>{{title}} · AdminDB</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
10
+ <link rel="stylesheet" href="{{basePath}}/css/app.css" />
11
+ <script>
12
+ window.APP = { basePath: {{{json basePath}}}, readonly: {{{json readonly}}} };
13
+ // Apply the saved theme before first paint to avoid a flash.
14
+ (function () {
15
+ try {
16
+ var t = localStorage.getItem('admindb-theme');
17
+ if (t === 'dark') document.documentElement.setAttribute('data-theme', 'admindbDark');
18
+ } catch (e) {}
19
+ })();
20
+ </script>
21
+ <script src="{{basePath}}/js/api.js" defer></script>
22
+ <script src="{{basePath}}/js/app.js" defer></script>
23
+ </head>
24
+ <body class="min-h-screen bg-base-200 text-base-content">
25
+ {{#if readonly}}
26
+ <div class="flex items-center justify-center gap-2 bg-warning/15 px-4 py-2 text-center text-sm font-semibold text-warning">
27
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
28
+ Read-only mode — this database is open without write access. All writes are disabled.
29
+ </div>
30
+ {{/if}}
31
+ <div class="drawer lg:drawer-open">
32
+ <input id="sidebar-toggle" type="checkbox" class="drawer-toggle" />
33
+ <div class="drawer-content flex min-h-screen flex-col">
34
+ {{> navbar}}
35
+ <main class="mx-auto w-full max-w-[1500px] flex-1 px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
36
+ {{{body}}}
37
+ </main>
38
+ <footer class="border-t border-base-300 px-6 py-4 text-center text-xs text-base-content/40">
39
+ AdminDB · a browser tool for SQLite databases
40
+ </footer>
41
+ </div>
42
+ <div class="drawer-side z-40">
43
+ <label for="sidebar-toggle" aria-label="close sidebar" class="drawer-overlay"></label>
44
+ {{> sidebar}}
45
+ </div>
46
+ </div>
47
+ <div id="toast-container" class="pointer-events-none fixed inset-x-0 top-4 z-50 flex flex-col items-center gap-2 px-4"></div>
48
+ </body>
49
+ </html>