sqlite-hub 0.1.3 → 0.3.1
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 +54 -26
- package/assets/mockups/graph_visualize.png +0 -0
- package/bin/sqlite-hub.js +85 -78
- package/changelog.md +11 -1
- package/index.html +3 -0
- package/js/api.js +14 -0
- package/js/app.js +42 -1
- package/js/components/queryEditor.js +8 -0
- package/js/components/structureGraph.js +925 -0
- package/js/lib/cytoscapeRuntime.js +16 -0
- package/js/store.js +55 -0
- package/js/views/connections.js +18 -3
- package/js/views/data.js +13 -0
- package/js/views/editor.js +1 -0
- package/js/views/structure.js +239 -117
- package/package.json +4 -1
- package/publish_brew.sh +22 -0
- package/server/routes/connections.js +15 -1
- package/server/routes/export.js +26 -6
- package/server/server.js +24 -2
- package/server/services/sqlite/backupService.js +112 -0
- package/server/services/sqlite/connectionManager.js +15 -0
- package/server/services/sqlite/exportService.js +46 -1
- package/server/services/sqlite/structureService.js +26 -0
- package/server/services/storage/appStateStore.js +135 -8
- package/server/utils/appPaths.js +131 -0
- 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,46 +1,74 @@
|
|
|
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
|
-
npm install sqlite-hub
|
|
26
|
-
```
|
|
23
|
+
## Features
|
|
27
24
|
|
|
28
|
-
|
|
25
|
+
### Structure view
|
|
29
26
|
|
|
30
|
-
|
|
27
|
+

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

|
|
34
|
+
|
|
35
|
+
Scan rows, sort fast, move through local data quickly, and export full tables as CSV.
|
|
36
|
+
|
|
37
|
+
### Row editing
|
|
35
38
|
|
|
36
|
-
|
|
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
|
|
37
60
|
|
|
38
61
|
```bash
|
|
39
|
-
|
|
62
|
+
brew tap oliverjessner/tap
|
|
63
|
+
brew install sqlite-hub
|
|
40
64
|
```
|
|
41
65
|
|
|
42
|
-
|
|
66
|
+
### NPM
|
|
43
67
|
|
|
44
68
|
```bash
|
|
45
|
-
|
|
69
|
+
npm install -g sqlit-hub
|
|
46
70
|
```
|
|
71
|
+
|
|
72
|
+
## Changelog
|
|
73
|
+
|
|
74
|
+
[Changelog](changelog.md)
|
|
Binary file
|
package/bin/sqlite-hub.js
CHANGED
|
@@ -1,116 +1,123 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const { spawn } = require(
|
|
3
|
+
const { spawn } = require('node:child_process');
|
|
4
4
|
|
|
5
5
|
const DEFAULT_PORT = 4173;
|
|
6
6
|
|
|
7
7
|
function printHelp() {
|
|
8
|
-
|
|
8
|
+
console.log(`SQLite Hub CLI
|
|
9
9
|
|
|
10
10
|
Usage:
|
|
11
11
|
sqlite-hub [--port:4173]
|
|
12
12
|
|
|
13
13
|
Options:
|
|
14
|
-
--port:PORT Start the server on a custom port.
|
|
15
14
|
--help Show this help text.
|
|
15
|
+
--port:PORT Start the server on a custom port.
|
|
16
|
+
--version Show the version number.
|
|
16
17
|
`);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
function parsePort(rawValue) {
|
|
20
|
-
|
|
21
|
+
const port = Number(rawValue);
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
24
|
+
throw new Error(`Invalid port: ${rawValue}`);
|
|
25
|
+
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
return port;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
function parseCliArguments(argv) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
let port;
|
|
32
|
+
|
|
33
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
34
|
+
const argument = argv[index];
|
|
35
|
+
|
|
36
|
+
if (argument === '--help' || argument === '-h') {
|
|
37
|
+
return { help: true };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (argument.startsWith('--port:')) {
|
|
41
|
+
port = parsePort(argument.slice('--port:'.length));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (argument.startsWith('--port=')) {
|
|
46
|
+
port = parsePort(argument.slice('--port='.length));
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (argument === '--port') {
|
|
51
|
+
port = parsePort(argv[index + 1]);
|
|
52
|
+
index += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (argument === '--version' || argument === '-v') {
|
|
57
|
+
const { version } = require('../package.json');
|
|
58
|
+
console.log(`SQLite Hub CLI version ${version}`);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
throw new Error(`Unknown argument: ${argument}`);
|
|
42
63
|
}
|
|
43
64
|
|
|
44
|
-
|
|
45
|
-
port = parsePort(argument.slice("--port=".length));
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (argument === "--port") {
|
|
50
|
-
port = parsePort(argv[index + 1]);
|
|
51
|
-
index += 1;
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
throw new Error(`Unknown argument: ${argument}`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return { help: false, port };
|
|
65
|
+
return { help: false, port };
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
function openInDefaultBrowser(url) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
69
|
+
const openers = {
|
|
70
|
+
darwin: {
|
|
71
|
+
command: 'open',
|
|
72
|
+
args: [url],
|
|
73
|
+
},
|
|
74
|
+
win32: {
|
|
75
|
+
command: 'cmd',
|
|
76
|
+
args: ['/c', 'start', '', url],
|
|
77
|
+
options: { windowsHide: true },
|
|
78
|
+
},
|
|
79
|
+
default: {
|
|
80
|
+
command: 'xdg-open',
|
|
81
|
+
args: [url],
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const opener = openers[process.platform] || openers.default;
|
|
86
|
+
const child = spawn(opener.command, opener.args, {
|
|
87
|
+
detached: true,
|
|
88
|
+
stdio: 'ignore',
|
|
89
|
+
...opener.options,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
child.on('error', error => {
|
|
93
|
+
console.warn(`Could not open the browser automatically: ${error.message}`);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
child.unref();
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
async function main() {
|
|
93
|
-
|
|
100
|
+
const { help, port = DEFAULT_PORT } = parseCliArguments(process.argv.slice(2));
|
|
94
101
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
if (help) {
|
|
103
|
+
printHelp();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
99
106
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
const { startServer } = require('../server/server');
|
|
108
|
+
const { url } = await startServer({ port });
|
|
109
|
+
openInDefaultBrowser(url);
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
if (require.main === module) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
main().catch(error => {
|
|
114
|
+
console.error(error.message);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
});
|
|
110
117
|
}
|
|
111
118
|
|
|
112
119
|
module.exports = {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
120
|
+
main,
|
|
121
|
+
openInDefaultBrowser,
|
|
122
|
+
parseCliArguments,
|
|
116
123
|
};
|
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
|
}
|
|
@@ -191,3 +197,11 @@ export function downloadQueryCsv(sql) {
|
|
|
191
197
|
fallbackFilename: "query-results.csv",
|
|
192
198
|
});
|
|
193
199
|
}
|
|
200
|
+
|
|
201
|
+
export function downloadTableCsv(tableName) {
|
|
202
|
+
return download("/api/export/table.csv", {
|
|
203
|
+
method: "POST",
|
|
204
|
+
body: { tableName },
|
|
205
|
+
fallbackFilename: `${tableName || "table"}.csv`,
|
|
206
|
+
});
|
|
207
|
+
}
|
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,6 +20,7 @@ import {
|
|
|
14
20
|
closeModal,
|
|
15
21
|
dismissToast,
|
|
16
22
|
executeCurrentQuery,
|
|
23
|
+
exportCurrentDataTableCsv,
|
|
17
24
|
exportCurrentQueryCsv,
|
|
18
25
|
getState,
|
|
19
26
|
initializeApp,
|
|
@@ -63,6 +70,10 @@ const shellRefs = {
|
|
|
63
70
|
toast: document.querySelector("#toast-root"),
|
|
64
71
|
};
|
|
65
72
|
|
|
73
|
+
function resetStructureGraphForDatabaseChange() {
|
|
74
|
+
resetPersistedStructureGraphState();
|
|
75
|
+
}
|
|
76
|
+
|
|
66
77
|
function renderQueryHighlightMarkup(query) {
|
|
67
78
|
if (query) {
|
|
68
79
|
return highlightSql(query);
|
|
@@ -212,6 +223,7 @@ function renderApp(state) {
|
|
|
212
223
|
const { main, panel } = resolveView(state);
|
|
213
224
|
const isLockedRoute = ["editor", "editorResults", "data"].includes(state.route.name);
|
|
214
225
|
|
|
226
|
+
teardownStructureGraph();
|
|
215
227
|
shellRefs.topNav.innerHTML = renderTopNav(state);
|
|
216
228
|
shellRefs.sidebar.innerHTML = renderSidebar(state);
|
|
217
229
|
shellRefs.statusBar.innerHTML = renderStatusBar(state);
|
|
@@ -222,6 +234,12 @@ function renderApp(state) {
|
|
|
222
234
|
shellRefs.toast.innerHTML = renderToasts(state.toasts);
|
|
223
235
|
shellRefs.shell.classList.toggle("panel-open", Boolean(panel));
|
|
224
236
|
restoreFocusedInputState(focusedInput);
|
|
237
|
+
|
|
238
|
+
if (state.route.name === "structure") {
|
|
239
|
+
mountStructureGraph(state).catch((error) => {
|
|
240
|
+
console.error("Failed to mount structure graph.", error);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
225
243
|
}
|
|
226
244
|
|
|
227
245
|
const router = createRouter((route) => {
|
|
@@ -251,6 +269,7 @@ async function handleAction(actionNode) {
|
|
|
251
269
|
dismissToast(actionNode.dataset.toastId);
|
|
252
270
|
return;
|
|
253
271
|
case "select-connection": {
|
|
272
|
+
resetStructureGraphForDatabaseChange();
|
|
254
273
|
const next = await selectConnection(actionNode.dataset.connectionId);
|
|
255
274
|
if (next) {
|
|
256
275
|
router.navigate("/overview");
|
|
@@ -258,6 +277,12 @@ async function handleAction(actionNode) {
|
|
|
258
277
|
return;
|
|
259
278
|
}
|
|
260
279
|
case "remove-connection": {
|
|
280
|
+
const isActiveConnection = getState().connections.active?.id === actionNode.dataset.connectionId;
|
|
281
|
+
|
|
282
|
+
if (isActiveConnection) {
|
|
283
|
+
resetStructureGraphForDatabaseChange();
|
|
284
|
+
}
|
|
285
|
+
|
|
261
286
|
const removed = await removeConnection(actionNode.dataset.connectionId);
|
|
262
287
|
if (removed) {
|
|
263
288
|
const nextState = getState();
|
|
@@ -269,6 +294,9 @@ async function handleAction(actionNode) {
|
|
|
269
294
|
}
|
|
270
295
|
return;
|
|
271
296
|
}
|
|
297
|
+
case "create-backup":
|
|
298
|
+
await createActiveConnectionBackup();
|
|
299
|
+
return;
|
|
272
300
|
case "execute-query": {
|
|
273
301
|
const success = await executeCurrentQuery();
|
|
274
302
|
router.navigate(success ? "/editor/results" : "/editor");
|
|
@@ -296,6 +324,9 @@ async function handleAction(actionNode) {
|
|
|
296
324
|
case "export-query-csv":
|
|
297
325
|
await exportCurrentQueryCsv();
|
|
298
326
|
return;
|
|
327
|
+
case "export-data-csv":
|
|
328
|
+
await exportCurrentDataTableCsv();
|
|
329
|
+
return;
|
|
299
330
|
case "select-structure-entry":
|
|
300
331
|
if (actionNode.dataset.entryName) {
|
|
301
332
|
await selectStructureEntry(actionNode.dataset.entryName);
|
|
@@ -422,6 +453,7 @@ document.addEventListener("submit", async (event) => {
|
|
|
422
453
|
|
|
423
454
|
switch (form.dataset.form) {
|
|
424
455
|
case "open-connection": {
|
|
456
|
+
resetStructureGraphForDatabaseChange();
|
|
425
457
|
const connection = await submitOpenConnection({
|
|
426
458
|
path: String(formData.get("path") ?? ""),
|
|
427
459
|
label: String(formData.get("label") ?? ""),
|
|
@@ -434,6 +466,7 @@ document.addEventListener("submit", async (event) => {
|
|
|
434
466
|
return;
|
|
435
467
|
}
|
|
436
468
|
case "create-connection": {
|
|
469
|
+
resetStructureGraphForDatabaseChange();
|
|
437
470
|
const connection = await submitCreateConnection({
|
|
438
471
|
path: String(formData.get("path") ?? ""),
|
|
439
472
|
label: String(formData.get("label") ?? ""),
|
|
@@ -445,6 +478,7 @@ document.addEventListener("submit", async (event) => {
|
|
|
445
478
|
return;
|
|
446
479
|
}
|
|
447
480
|
case "import-sql": {
|
|
481
|
+
resetStructureGraphForDatabaseChange();
|
|
448
482
|
const targetMode = String(formData.get("targetMode") ?? "active");
|
|
449
483
|
const payload = {
|
|
450
484
|
sqlFilePath: String(formData.get("sqlFilePath") ?? ""),
|
|
@@ -467,7 +501,14 @@ document.addEventListener("submit", async (event) => {
|
|
|
467
501
|
return;
|
|
468
502
|
}
|
|
469
503
|
case "edit-connection": {
|
|
470
|
-
|
|
504
|
+
const connectionId = String(formData.get("connectionId") ?? "");
|
|
505
|
+
const isActiveConnection = getState().connections.active?.id === connectionId;
|
|
506
|
+
|
|
507
|
+
if (isActiveConnection) {
|
|
508
|
+
resetStructureGraphForDatabaseChange();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
await submitEditConnection(connectionId, {
|
|
471
512
|
path: String(formData.get("path") ?? ""),
|
|
472
513
|
label: String(formData.get("label") ?? ""),
|
|
473
514
|
readOnly: formData.get("readOnly") === "on",
|
|
@@ -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"
|