sqlite-hub 0.2.0 → 0.3.2
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/.github/funding.yml +2 -0
- package/LICENSE +21 -0
- package/README.md +57 -25
- package/assets/mockups/graph_visualize.png +0 -0
- package/changelog.md +12 -1
- package/index.html +3 -0
- package/js/api.js +21 -0
- package/js/app.js +54 -1
- package/js/components/modal.js +74 -0
- package/js/components/queryEditor.js +8 -0
- package/js/components/rowEditorPanel.js +63 -33
- package/js/components/structureGraph.js +925 -0
- package/js/lib/cytoscapeRuntime.js +16 -0
- package/js/store.js +259 -1
- package/js/views/connections.js +18 -3
- package/js/views/data.js +17 -0
- package/js/views/editor.js +5 -0
- package/js/views/structure.js +239 -117
- package/package.json +5 -2
- package/publish_brew.sh +22 -0
- package/server/routes/connections.js +15 -1
- package/server/routes/data.js +14 -0
- package/server/routes/export.js +26 -6
- package/server/server.js +38 -1
- package/server/services/sqlite/backupService.js +112 -0
- package/server/services/sqlite/connectionManager.js +15 -0
- package/server/services/sqlite/dataBrowserService.js +30 -0
- package/server/services/sqlite/exportService.js +46 -1
- package/server/services/sqlite/structureService.js +26 -0
- package/server/services/storage/appStateStore.js +7 -2
- package/styles/structure-graph.css +414 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 oliverjessner
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,48 +1,80 @@
|
|
|
1
|
-
# sqlite-hub
|
|
2
|
-
|
|
3
|
-
A cyberpunk inspired management app for sqlite
|
|
1
|
+
# sqlite-hub ⚡️
|
|
4
2
|
|
|
5
3
|

|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
A focused local-first app for browsing, editing, and querying SQLite databases.
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
SQLite Hub is built for developers and technical users who want a clean SQLite workflow without heavy database clients, cloud layers, or dashboard noise.
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
brew install oliverjessner/tap/sqlite-hub
|
|
13
|
-
```
|
|
9
|
+
## Why SQLite Hub?
|
|
14
10
|
|
|
15
|
-
|
|
11
|
+
Many database tools are powerful, but feel oversized when all you want is to inspect a local SQLite file, edit a few rows, and run a query fast.
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
brew tap oliverjessner/tap
|
|
19
|
-
brew install sqlite-hub
|
|
20
|
-
```
|
|
13
|
+
SQLite Hub keeps that workflow sharp:
|
|
21
14
|
|
|
22
|
-
|
|
15
|
+
- browse tables and rows
|
|
16
|
+
- inspect schema and structure
|
|
17
|
+
- edit records in place
|
|
18
|
+
- export tables and query results as CSV
|
|
19
|
+
- create simple local backups of the active database
|
|
20
|
+
- run SQL in a syntax-highlighted editor
|
|
21
|
+
- stay local and move fast
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
### Structure view
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
Inspect tables, columns, types, and schema details without losing pace. Visualized in a graph.
|
|
30
|
+
|
|
31
|
+
### Data browser
|
|
32
|
+
|
|
33
|
+

|
|
27
34
|
|
|
28
|
-
|
|
35
|
+
Scan rows, sort fast, move through local data quickly, and export full tables as CSV.
|
|
29
36
|
|
|
30
|
-
|
|
37
|
+
### Row editing
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
Open one record, edit it in place, commit, continue.
|
|
42
|
+
|
|
43
|
+
### SQL editor
|
|
44
|
+
|
|
45
|
+

|
|
46
|
+
|
|
47
|
+
Write queries in a syntax-highlighted editor, inspect results in the same workflow, and export result sets as CSV.
|
|
48
|
+
|
|
49
|
+
### Simple backups
|
|
50
|
+
|
|
51
|
+
Create timestamped local backups of the active SQLite database in one click. Backups are stored as plain file copies in a local `backups` folder next to the database.
|
|
52
|
+
|
|
53
|
+
### Local-first
|
|
54
|
+
|
|
55
|
+
Built around local SQLite files, not hosted dashboards or team complexity.
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
### Homebrew
|
|
31
60
|
|
|
32
61
|
```bash
|
|
33
|
-
|
|
62
|
+
brew tap oliverjessner/tap
|
|
63
|
+
brew install sqlite-hub
|
|
34
64
|
```
|
|
35
65
|
|
|
36
|
-
|
|
66
|
+
### NPM
|
|
37
67
|
|
|
38
68
|
```bash
|
|
39
|
-
npm
|
|
69
|
+
npm install -g sqlit-hub
|
|
40
70
|
```
|
|
41
71
|
|
|
42
|
-
|
|
72
|
+
## alternative port
|
|
43
73
|
|
|
44
74
|
```bash
|
|
45
|
-
|
|
75
|
+
sqlit-hub --port:4174
|
|
46
76
|
```
|
|
47
77
|
|
|
48
|
-
|
|
78
|
+
## Changelog
|
|
79
|
+
|
|
80
|
+
[Changelog](/changelog.md)
|
|
Binary file
|
package/changelog.md
CHANGED
package/index.html
CHANGED
|
@@ -90,11 +90,14 @@
|
|
|
90
90
|
<link href="styles/layout.css" rel="stylesheet" />
|
|
91
91
|
<link href="styles/components.css" rel="stylesheet" />
|
|
92
92
|
<link href="styles/views.css" rel="stylesheet" />
|
|
93
|
+
<link href="styles/structure-graph.css" rel="stylesheet" />
|
|
93
94
|
</head>
|
|
94
95
|
<body
|
|
95
96
|
class="bg-background text-on-surface font-body selection:bg-primary-container selection:text-on-primary overflow-hidden"
|
|
96
97
|
>
|
|
97
98
|
<div id="app"></div>
|
|
99
|
+
<script src="/vendor/elkjs/lib/elk.bundled.js"></script>
|
|
100
|
+
<script src="/vendor/cytoscape-elk/dist/cytoscape-elk.js"></script>
|
|
98
101
|
<script src="js/app.js" type="module"></script>
|
|
99
102
|
</body>
|
|
100
103
|
</html>
|
package/js/api.js
CHANGED
|
@@ -113,6 +113,12 @@ export function updateRecentConnection(id, payload) {
|
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
export function createActiveConnectionBackup() {
|
|
117
|
+
return request("/api/connections/backup-active", {
|
|
118
|
+
method: "POST",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
116
122
|
export function getOverview() {
|
|
117
123
|
return request("/api/db/overview");
|
|
118
124
|
}
|
|
@@ -173,6 +179,13 @@ export function updateDataTableRow(tableName, payload) {
|
|
|
173
179
|
});
|
|
174
180
|
}
|
|
175
181
|
|
|
182
|
+
export function deleteDataTableRow(tableName, payload) {
|
|
183
|
+
return request(`/api/data/${encodeURIComponent(tableName)}/rows`, {
|
|
184
|
+
method: "DELETE",
|
|
185
|
+
body: payload,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
176
189
|
export function getSettings() {
|
|
177
190
|
return request("/api/settings");
|
|
178
191
|
}
|
|
@@ -191,3 +204,11 @@ export function downloadQueryCsv(sql) {
|
|
|
191
204
|
fallbackFilename: "query-results.csv",
|
|
192
205
|
});
|
|
193
206
|
}
|
|
207
|
+
|
|
208
|
+
export function downloadTableCsv(tableName) {
|
|
209
|
+
return download("/api/export/table.csv", {
|
|
210
|
+
method: "POST",
|
|
211
|
+
body: { tableName },
|
|
212
|
+
fallbackFilename: `${tableName || "table"}.csv`,
|
|
213
|
+
});
|
|
214
|
+
}
|
package/js/app.js
CHANGED
|
@@ -2,10 +2,16 @@ import { renderAppShell } from "./components/appShell.js";
|
|
|
2
2
|
import { renderModal } from "./components/modal.js";
|
|
3
3
|
import { renderSidebar } from "./components/sidebar.js";
|
|
4
4
|
import { renderStatusBar } from "./components/statusBar.js";
|
|
5
|
+
import {
|
|
6
|
+
mountStructureGraph,
|
|
7
|
+
resetPersistedStructureGraphState,
|
|
8
|
+
teardownStructureGraph,
|
|
9
|
+
} from "./components/structureGraph.js";
|
|
5
10
|
import { renderToasts } from "./components/toast.js";
|
|
6
11
|
import { renderTopNav } from "./components/topNav.js";
|
|
7
12
|
import { createRouter } from "./router.js";
|
|
8
13
|
import {
|
|
14
|
+
createActiveConnectionBackup,
|
|
9
15
|
clearCurrentQuery,
|
|
10
16
|
clearDataRowSelection,
|
|
11
17
|
clearEditorRowSelection,
|
|
@@ -14,11 +20,14 @@ import {
|
|
|
14
20
|
closeModal,
|
|
15
21
|
dismissToast,
|
|
16
22
|
executeCurrentQuery,
|
|
23
|
+
exportCurrentDataTableCsv,
|
|
17
24
|
exportCurrentQueryCsv,
|
|
18
25
|
getState,
|
|
19
26
|
initializeApp,
|
|
20
27
|
loadQueryFromHistory,
|
|
21
28
|
openModal,
|
|
29
|
+
openDeleteDataRowModal,
|
|
30
|
+
openDeleteEditorRowModal,
|
|
22
31
|
openEditConnectionModal,
|
|
23
32
|
refreshCurrentRoute,
|
|
24
33
|
removeConnection,
|
|
@@ -32,6 +41,7 @@ import {
|
|
|
32
41
|
setEditorTab,
|
|
33
42
|
setRoute,
|
|
34
43
|
submitCreateConnection,
|
|
44
|
+
submitDeleteRowConfirmation,
|
|
35
45
|
submitDataRowUpdate,
|
|
36
46
|
submitEditorRowUpdate,
|
|
37
47
|
submitEditConnection,
|
|
@@ -63,6 +73,10 @@ const shellRefs = {
|
|
|
63
73
|
toast: document.querySelector("#toast-root"),
|
|
64
74
|
};
|
|
65
75
|
|
|
76
|
+
function resetStructureGraphForDatabaseChange() {
|
|
77
|
+
resetPersistedStructureGraphState();
|
|
78
|
+
}
|
|
79
|
+
|
|
66
80
|
function renderQueryHighlightMarkup(query) {
|
|
67
81
|
if (query) {
|
|
68
82
|
return highlightSql(query);
|
|
@@ -212,6 +226,7 @@ function renderApp(state) {
|
|
|
212
226
|
const { main, panel } = resolveView(state);
|
|
213
227
|
const isLockedRoute = ["editor", "editorResults", "data"].includes(state.route.name);
|
|
214
228
|
|
|
229
|
+
teardownStructureGraph();
|
|
215
230
|
shellRefs.topNav.innerHTML = renderTopNav(state);
|
|
216
231
|
shellRefs.sidebar.innerHTML = renderSidebar(state);
|
|
217
232
|
shellRefs.statusBar.innerHTML = renderStatusBar(state);
|
|
@@ -222,6 +237,12 @@ function renderApp(state) {
|
|
|
222
237
|
shellRefs.toast.innerHTML = renderToasts(state.toasts);
|
|
223
238
|
shellRefs.shell.classList.toggle("panel-open", Boolean(panel));
|
|
224
239
|
restoreFocusedInputState(focusedInput);
|
|
240
|
+
|
|
241
|
+
if (state.route.name === "structure") {
|
|
242
|
+
mountStructureGraph(state).catch((error) => {
|
|
243
|
+
console.error("Failed to mount structure graph.", error);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
225
246
|
}
|
|
226
247
|
|
|
227
248
|
const router = createRouter((route) => {
|
|
@@ -251,6 +272,7 @@ async function handleAction(actionNode) {
|
|
|
251
272
|
dismissToast(actionNode.dataset.toastId);
|
|
252
273
|
return;
|
|
253
274
|
case "select-connection": {
|
|
275
|
+
resetStructureGraphForDatabaseChange();
|
|
254
276
|
const next = await selectConnection(actionNode.dataset.connectionId);
|
|
255
277
|
if (next) {
|
|
256
278
|
router.navigate("/overview");
|
|
@@ -258,6 +280,12 @@ async function handleAction(actionNode) {
|
|
|
258
280
|
return;
|
|
259
281
|
}
|
|
260
282
|
case "remove-connection": {
|
|
283
|
+
const isActiveConnection = getState().connections.active?.id === actionNode.dataset.connectionId;
|
|
284
|
+
|
|
285
|
+
if (isActiveConnection) {
|
|
286
|
+
resetStructureGraphForDatabaseChange();
|
|
287
|
+
}
|
|
288
|
+
|
|
261
289
|
const removed = await removeConnection(actionNode.dataset.connectionId);
|
|
262
290
|
if (removed) {
|
|
263
291
|
const nextState = getState();
|
|
@@ -269,11 +297,20 @@ async function handleAction(actionNode) {
|
|
|
269
297
|
}
|
|
270
298
|
return;
|
|
271
299
|
}
|
|
300
|
+
case "create-backup":
|
|
301
|
+
await createActiveConnectionBackup();
|
|
302
|
+
return;
|
|
272
303
|
case "execute-query": {
|
|
273
304
|
const success = await executeCurrentQuery();
|
|
274
305
|
router.navigate(success ? "/editor/results" : "/editor");
|
|
275
306
|
return;
|
|
276
307
|
}
|
|
308
|
+
case "delete-data-row":
|
|
309
|
+
openDeleteDataRowModal(actionNode.dataset.rowIndex);
|
|
310
|
+
return;
|
|
311
|
+
case "delete-editor-row":
|
|
312
|
+
openDeleteEditorRowModal(actionNode.dataset.rowIndex);
|
|
313
|
+
return;
|
|
277
314
|
case "clear-query":
|
|
278
315
|
clearCurrentQuery();
|
|
279
316
|
return;
|
|
@@ -296,6 +333,9 @@ async function handleAction(actionNode) {
|
|
|
296
333
|
case "export-query-csv":
|
|
297
334
|
await exportCurrentQueryCsv();
|
|
298
335
|
return;
|
|
336
|
+
case "export-data-csv":
|
|
337
|
+
await exportCurrentDataTableCsv();
|
|
338
|
+
return;
|
|
299
339
|
case "select-structure-entry":
|
|
300
340
|
if (actionNode.dataset.entryName) {
|
|
301
341
|
await selectStructureEntry(actionNode.dataset.entryName);
|
|
@@ -422,6 +462,7 @@ document.addEventListener("submit", async (event) => {
|
|
|
422
462
|
|
|
423
463
|
switch (form.dataset.form) {
|
|
424
464
|
case "open-connection": {
|
|
465
|
+
resetStructureGraphForDatabaseChange();
|
|
425
466
|
const connection = await submitOpenConnection({
|
|
426
467
|
path: String(formData.get("path") ?? ""),
|
|
427
468
|
label: String(formData.get("label") ?? ""),
|
|
@@ -434,6 +475,7 @@ document.addEventListener("submit", async (event) => {
|
|
|
434
475
|
return;
|
|
435
476
|
}
|
|
436
477
|
case "create-connection": {
|
|
478
|
+
resetStructureGraphForDatabaseChange();
|
|
437
479
|
const connection = await submitCreateConnection({
|
|
438
480
|
path: String(formData.get("path") ?? ""),
|
|
439
481
|
label: String(formData.get("label") ?? ""),
|
|
@@ -445,6 +487,7 @@ document.addEventListener("submit", async (event) => {
|
|
|
445
487
|
return;
|
|
446
488
|
}
|
|
447
489
|
case "import-sql": {
|
|
490
|
+
resetStructureGraphForDatabaseChange();
|
|
448
491
|
const targetMode = String(formData.get("targetMode") ?? "active");
|
|
449
492
|
const payload = {
|
|
450
493
|
sqlFilePath: String(formData.get("sqlFilePath") ?? ""),
|
|
@@ -467,7 +510,14 @@ document.addEventListener("submit", async (event) => {
|
|
|
467
510
|
return;
|
|
468
511
|
}
|
|
469
512
|
case "edit-connection": {
|
|
470
|
-
|
|
513
|
+
const connectionId = String(formData.get("connectionId") ?? "");
|
|
514
|
+
const isActiveConnection = getState().connections.active?.id === connectionId;
|
|
515
|
+
|
|
516
|
+
if (isActiveConnection) {
|
|
517
|
+
resetStructureGraphForDatabaseChange();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
await submitEditConnection(connectionId, {
|
|
471
521
|
path: String(formData.get("path") ?? ""),
|
|
472
522
|
label: String(formData.get("label") ?? ""),
|
|
473
523
|
readOnly: formData.get("readOnly") === "on",
|
|
@@ -475,6 +525,9 @@ document.addEventListener("submit", async (event) => {
|
|
|
475
525
|
|
|
476
526
|
return;
|
|
477
527
|
}
|
|
528
|
+
case "delete-row-confirm":
|
|
529
|
+
await submitDeleteRowConfirmation();
|
|
530
|
+
return;
|
|
478
531
|
case "save-data-row": {
|
|
479
532
|
const values = {};
|
|
480
533
|
|
package/js/components/modal.js
CHANGED
|
@@ -254,6 +254,75 @@ function renderImportSqlForm(modal, state) {
|
|
|
254
254
|
`;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
function renderDeleteRowConfirmForm(modal) {
|
|
258
|
+
const rowPreview = modal.rowPreview ?? [];
|
|
259
|
+
|
|
260
|
+
return `
|
|
261
|
+
<form class="space-y-5" data-form="delete-row-confirm">
|
|
262
|
+
<div class="space-y-3">
|
|
263
|
+
<p class="text-sm leading-7 text-on-surface">
|
|
264
|
+
Delete this row from <span class="font-bold text-primary-container">${escapeHtml(
|
|
265
|
+
modal.tableName ?? "the current table"
|
|
266
|
+
)}</span>?
|
|
267
|
+
</p>
|
|
268
|
+
<p class="text-sm leading-7 text-on-surface-variant/65">
|
|
269
|
+
This action cannot be undone.
|
|
270
|
+
</p>
|
|
271
|
+
${
|
|
272
|
+
modal.rowLabel
|
|
273
|
+
? `
|
|
274
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
275
|
+
${escapeHtml(modal.rowLabel)}
|
|
276
|
+
</div>
|
|
277
|
+
`
|
|
278
|
+
: ""
|
|
279
|
+
}
|
|
280
|
+
${
|
|
281
|
+
rowPreview.length
|
|
282
|
+
? `
|
|
283
|
+
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
284
|
+
${rowPreview
|
|
285
|
+
.map(
|
|
286
|
+
(field) => `
|
|
287
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
|
|
288
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
289
|
+
${escapeHtml(field.label)}
|
|
290
|
+
</div>
|
|
291
|
+
<div
|
|
292
|
+
class="mt-2 text-sm text-on-surface"
|
|
293
|
+
title="${escapeHtml(field.fullValue ?? field.value ?? "")}"
|
|
294
|
+
>
|
|
295
|
+
${escapeHtml(field.value ?? "")}
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
`
|
|
299
|
+
)
|
|
300
|
+
.join("")}
|
|
301
|
+
</div>
|
|
302
|
+
`
|
|
303
|
+
: ""
|
|
304
|
+
}
|
|
305
|
+
</div>
|
|
306
|
+
${renderError(modal.error)}
|
|
307
|
+
<div class="flex items-center justify-end gap-3 pt-2">
|
|
308
|
+
<button
|
|
309
|
+
class="border border-outline-variant/20 px-4 py-3 text-xs font-bold uppercase tracking-[0.18em] text-on-surface-variant hover:bg-surface-container-highest"
|
|
310
|
+
data-action="close-modal"
|
|
311
|
+
type="button"
|
|
312
|
+
>
|
|
313
|
+
Cancel
|
|
314
|
+
</button>
|
|
315
|
+
<button
|
|
316
|
+
class="border border-error/25 bg-error-container/10 px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-error"
|
|
317
|
+
type="submit"
|
|
318
|
+
>
|
|
319
|
+
${modal.submitting ? "Deleting..." : "Delete Row"}
|
|
320
|
+
</button>
|
|
321
|
+
</div>
|
|
322
|
+
</form>
|
|
323
|
+
`;
|
|
324
|
+
}
|
|
325
|
+
|
|
257
326
|
export function renderModal(state) {
|
|
258
327
|
const modal = state.modal;
|
|
259
328
|
|
|
@@ -282,6 +351,11 @@ export function renderModal(state) {
|
|
|
282
351
|
title: "Edit Connection",
|
|
283
352
|
body: renderEditConnectionForm(modal),
|
|
284
353
|
},
|
|
354
|
+
"delete-row": {
|
|
355
|
+
eyebrow: "Mutation // Confirm row deletion",
|
|
356
|
+
title: "Delete Row",
|
|
357
|
+
body: renderDeleteRowConfirmForm(modal),
|
|
358
|
+
},
|
|
285
359
|
};
|
|
286
360
|
|
|
287
361
|
const config = contentByKind[modal.kind];
|
|
@@ -38,6 +38,7 @@ export function renderQueryEditor({
|
|
|
38
38
|
query,
|
|
39
39
|
title,
|
|
40
40
|
executing = false,
|
|
41
|
+
exporting = false,
|
|
41
42
|
history = [],
|
|
42
43
|
historyLoading = false,
|
|
43
44
|
}) {
|
|
@@ -75,6 +76,13 @@ export function renderQueryEditor({
|
|
|
75
76
|
>
|
|
76
77
|
Clear
|
|
77
78
|
</button>
|
|
79
|
+
<button
|
|
80
|
+
class="px-4 py-1.5 text-[10px] font-bold uppercase tracking-widest text-on-surface hover:bg-surface-container-highest transition-colors"
|
|
81
|
+
data-action="export-query-csv"
|
|
82
|
+
type="button"
|
|
83
|
+
>
|
|
84
|
+
${exporting ? "Exporting..." : "Export CSV"}
|
|
85
|
+
</button>
|
|
78
86
|
<button
|
|
79
87
|
class="bg-primary-container px-6 py-1.5 text-xs font-black uppercase tracking-tighter text-on-primary shadow-[0_0_15px_-5px_rgba(252,227,0,0.4)] transition-all hover:brightness-110"
|
|
80
88
|
data-action="execute-query"
|
|
@@ -23,16 +23,23 @@ export function renderRowEditorPanel({
|
|
|
23
23
|
disabledMessage = "",
|
|
24
24
|
saveError = null,
|
|
25
25
|
saving = false,
|
|
26
|
+
deleting = false,
|
|
26
27
|
reloadAction = "",
|
|
27
|
-
submitLabel = "Save
|
|
28
|
+
submitLabel = "Save",
|
|
29
|
+
deleteAction = "",
|
|
30
|
+
deleteRowIndex = null,
|
|
31
|
+
deleteLabel = "Delete Row",
|
|
32
|
+
deleteEnabled = false,
|
|
28
33
|
emptyEditableMessage = "This row has no editable scalar columns.",
|
|
29
34
|
}) {
|
|
30
35
|
const canSubmit = !disabledMessage && editableFields.length > 0;
|
|
36
|
+
const canDelete = !disabledMessage && deleteEnabled;
|
|
37
|
+
const formId = `${formName}-panel-form`;
|
|
31
38
|
|
|
32
39
|
return `
|
|
33
40
|
<section class="flex h-full min-h-0 flex-col bg-surface-low">
|
|
34
41
|
<header class="border-b border-outline-variant/10 bg-surface-container px-6 py-5">
|
|
35
|
-
<div class="
|
|
42
|
+
<div class="space-y-4">
|
|
36
43
|
<div>
|
|
37
44
|
<div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
|
|
38
45
|
${escapeHtml(sectionLabel)}
|
|
@@ -50,13 +57,58 @@ export function renderRowEditorPanel({
|
|
|
50
57
|
: ""
|
|
51
58
|
}
|
|
52
59
|
</div>
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
<div class="flex flex-wrap items-center justify-end gap-2">
|
|
61
|
+
${
|
|
62
|
+
reloadAction
|
|
63
|
+
? `
|
|
64
|
+
<button
|
|
65
|
+
class="border border-outline-variant/20 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface hover:bg-surface-container-highest"
|
|
66
|
+
data-action="${escapeHtml(reloadAction)}"
|
|
67
|
+
type="button"
|
|
68
|
+
>
|
|
69
|
+
Reload
|
|
70
|
+
</button>
|
|
71
|
+
`
|
|
72
|
+
: ""
|
|
73
|
+
}
|
|
74
|
+
${
|
|
75
|
+
canSubmit
|
|
76
|
+
? `
|
|
77
|
+
<button
|
|
78
|
+
class="bg-primary-container px-5 py-3 text-[10px] font-black uppercase tracking-[0.16em] text-on-primary disabled:cursor-default disabled:opacity-40"
|
|
79
|
+
form="${escapeHtml(formId)}"
|
|
80
|
+
type="submit"
|
|
81
|
+
${saving || deleting ? "disabled" : ""}
|
|
82
|
+
>
|
|
83
|
+
${escapeHtml(saving ? "Saving..." : submitLabel)}
|
|
84
|
+
</button>
|
|
85
|
+
`
|
|
86
|
+
: ""
|
|
87
|
+
}
|
|
88
|
+
${
|
|
89
|
+
canDelete
|
|
90
|
+
? `
|
|
91
|
+
<button
|
|
92
|
+
class="border border-error/25 bg-error-container/10 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-error transition-colors hover:bg-error-container/20 disabled:cursor-default disabled:opacity-40"
|
|
93
|
+
data-action="${escapeHtml(deleteAction)}"
|
|
94
|
+
data-row-index="${escapeHtml(String(deleteRowIndex ?? ""))}"
|
|
95
|
+
type="button"
|
|
96
|
+
${saving || deleting ? "disabled" : ""}
|
|
97
|
+
>
|
|
98
|
+
${escapeHtml(deleting ? "Deleting..." : deleteLabel)}
|
|
99
|
+
</button>
|
|
100
|
+
`
|
|
101
|
+
: ""
|
|
102
|
+
}
|
|
103
|
+
<button
|
|
104
|
+
aria-label="Close panel"
|
|
105
|
+
class="flex h-11 w-11 items-center justify-center border border-outline-variant/20 text-on-surface hover:bg-surface-container-highest"
|
|
106
|
+
data-action="${escapeHtml(closeAction)}"
|
|
107
|
+
type="button"
|
|
108
|
+
>
|
|
109
|
+
<span class="material-symbols-outlined text-base">close</span>
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
60
112
|
</div>
|
|
61
113
|
</header>
|
|
62
114
|
<div class="custom-scrollbar flex-1 overflow-auto px-6 py-6">
|
|
@@ -68,7 +120,7 @@ export function renderRowEditorPanel({
|
|
|
68
120
|
</div>
|
|
69
121
|
`
|
|
70
122
|
: `
|
|
71
|
-
<form class="space-y-6" data-form="${escapeHtml(formName)}">
|
|
123
|
+
<form class="space-y-6" data-form="${escapeHtml(formName)}" id="${escapeHtml(formId)}">
|
|
72
124
|
${hiddenFields
|
|
73
125
|
.map(
|
|
74
126
|
(field) => `
|
|
@@ -92,7 +144,7 @@ export function renderRowEditorPanel({
|
|
|
92
144
|
${escapeHtml(field.label ?? field.name)}
|
|
93
145
|
</span>
|
|
94
146
|
<textarea
|
|
95
|
-
class="min-h-[
|
|
147
|
+
class="min-h-[56px] w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
96
148
|
name="field:${escapeHtml(field.name)}"
|
|
97
149
|
>${escapeHtml(field.value ?? "")}</textarea>
|
|
98
150
|
</label>
|
|
@@ -117,28 +169,6 @@ export function renderRowEditorPanel({
|
|
|
117
169
|
`
|
|
118
170
|
: ""
|
|
119
171
|
}
|
|
120
|
-
<div class="flex items-center justify-end gap-3 border-t border-outline-variant/10 pt-6">
|
|
121
|
-
${
|
|
122
|
-
reloadAction
|
|
123
|
-
? `
|
|
124
|
-
<button
|
|
125
|
-
class="border border-outline-variant/20 px-4 py-3 text-xs font-bold uppercase tracking-[0.18em] text-on-surface hover:bg-surface-container-highest"
|
|
126
|
-
data-action="${escapeHtml(reloadAction)}"
|
|
127
|
-
type="button"
|
|
128
|
-
>
|
|
129
|
-
Reload
|
|
130
|
-
</button>
|
|
131
|
-
`
|
|
132
|
-
: ""
|
|
133
|
-
}
|
|
134
|
-
<button
|
|
135
|
-
class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary disabled:cursor-default disabled:opacity-40"
|
|
136
|
-
type="submit"
|
|
137
|
-
${canSubmit ? "" : "disabled"}
|
|
138
|
-
>
|
|
139
|
-
${escapeHtml(saving ? "Saving..." : submitLabel)}
|
|
140
|
-
</button>
|
|
141
|
-
</div>
|
|
142
172
|
</form>
|
|
143
173
|
`
|
|
144
174
|
}
|