sqlite-hub 0.1.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/.npmingnore +4 -0
- package/README.md +46 -0
- package/assets/images/logo.webp +0 -0
- package/assets/images/logo_extrasmall.webp +0 -0
- package/assets/images/logo_raw.png +0 -0
- package/assets/images/logo_small.webp +0 -0
- package/assets/mockups/connections.png +0 -0
- package/assets/mockups/data.png +0 -0
- package/assets/mockups/data_edit.png +0 -0
- package/assets/mockups/home.png +0 -0
- package/assets/mockups/overview.png +0 -0
- package/assets/mockups/sql_editor.png +0 -0
- package/assets/mockups/structure.png +0 -0
- package/bin/sqlite-hub.js +116 -0
- package/changelog.md +3 -0
- package/data/.gitkeep +0 -0
- package/index.html +100 -0
- package/js/api.js +193 -0
- package/js/app.js +520 -0
- package/js/components/actionBar.js +8 -0
- package/js/components/appShell.js +17 -0
- package/js/components/badges.js +5 -0
- package/js/components/bottomTabs.js +37 -0
- package/js/components/connectionCard.js +106 -0
- package/js/components/dataGrid.js +47 -0
- package/js/components/emptyState.js +159 -0
- package/js/components/metricCard.js +32 -0
- package/js/components/modal.js +317 -0
- package/js/components/pageHeader.js +33 -0
- package/js/components/queryEditor.js +121 -0
- package/js/components/queryResults.js +107 -0
- package/js/components/rowEditorPanel.js +164 -0
- package/js/components/sidebar.js +57 -0
- package/js/components/statusBar.js +39 -0
- package/js/components/toast.js +39 -0
- package/js/components/topNav.js +27 -0
- package/js/router.js +66 -0
- package/js/store.js +1092 -0
- package/js/utils/format.js +179 -0
- package/js/views/connections.js +133 -0
- package/js/views/data.js +400 -0
- package/js/views/editor.js +259 -0
- package/js/views/landing.js +11 -0
- package/js/views/overview.js +220 -0
- package/js/views/settings.js +109 -0
- package/js/views/structure.js +242 -0
- package/package.json +18 -0
- package/publish_brew.sh +444 -0
- package/publish_npm.sh +241 -0
- package/server/routes/connections.js +146 -0
- package/server/routes/data.js +59 -0
- package/server/routes/export.js +25 -0
- package/server/routes/overview.js +39 -0
- package/server/routes/settings.js +50 -0
- package/server/routes/sql.js +50 -0
- package/server/routes/structure.js +38 -0
- package/server/server.js +136 -0
- package/server/services/sqlite/connectionManager.js +306 -0
- package/server/services/sqlite/dataBrowserService.js +255 -0
- package/server/services/sqlite/exportService.js +34 -0
- package/server/services/sqlite/importService.js +111 -0
- package/server/services/sqlite/introspection.js +302 -0
- package/server/services/sqlite/overviewService.js +109 -0
- package/server/services/sqlite/sqlExecutor.js +434 -0
- package/server/services/sqlite/structureService.js +60 -0
- package/server/services/storage/appStateStore.js +530 -0
- package/server/utils/csv.js +34 -0
- package/server/utils/errors.js +175 -0
- package/server/utils/fileValidation.js +135 -0
- package/server/utils/identifier.js +38 -0
- package/server/utils/sqliteTypes.js +112 -0
- package/styles/base.css +176 -0
- package/styles/components.css +323 -0
- package/styles/layout.css +101 -0
- package/styles/tokens.css +49 -0
- package/styles/views.css +84 -0
package/js/app.js
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import { renderAppShell } from "./components/appShell.js";
|
|
2
|
+
import { renderModal } from "./components/modal.js";
|
|
3
|
+
import { renderSidebar } from "./components/sidebar.js";
|
|
4
|
+
import { renderStatusBar } from "./components/statusBar.js";
|
|
5
|
+
import { renderToasts } from "./components/toast.js";
|
|
6
|
+
import { renderTopNav } from "./components/topNav.js";
|
|
7
|
+
import { createRouter } from "./router.js";
|
|
8
|
+
import {
|
|
9
|
+
clearCurrentQuery,
|
|
10
|
+
clearDataRowSelection,
|
|
11
|
+
clearEditorRowSelection,
|
|
12
|
+
clearEditorResults,
|
|
13
|
+
clearSqlHistoryStateAndData,
|
|
14
|
+
closeModal,
|
|
15
|
+
dismissToast,
|
|
16
|
+
executeCurrentQuery,
|
|
17
|
+
exportCurrentQueryCsv,
|
|
18
|
+
getState,
|
|
19
|
+
initializeApp,
|
|
20
|
+
loadQueryFromHistory,
|
|
21
|
+
openModal,
|
|
22
|
+
openEditConnectionModal,
|
|
23
|
+
refreshCurrentRoute,
|
|
24
|
+
removeConnection,
|
|
25
|
+
selectDataRow,
|
|
26
|
+
selectEditorRow,
|
|
27
|
+
selectConnection,
|
|
28
|
+
selectStructureEntry,
|
|
29
|
+
setDataPage,
|
|
30
|
+
setDataPageSize,
|
|
31
|
+
setCurrentQuery,
|
|
32
|
+
setEditorTab,
|
|
33
|
+
setRoute,
|
|
34
|
+
submitCreateConnection,
|
|
35
|
+
submitDataRowUpdate,
|
|
36
|
+
submitEditorRowUpdate,
|
|
37
|
+
submitEditConnection,
|
|
38
|
+
submitImportSql,
|
|
39
|
+
submitOpenConnection,
|
|
40
|
+
subscribe,
|
|
41
|
+
} from "./store.js";
|
|
42
|
+
import { renderConnectionsView } from "./views/connections.js";
|
|
43
|
+
import { renderDataView } from "./views/data.js";
|
|
44
|
+
import { renderEditorView } from "./views/editor.js";
|
|
45
|
+
import { renderLandingView } from "./views/landing.js";
|
|
46
|
+
import { renderOverviewView } from "./views/overview.js";
|
|
47
|
+
import { renderSettingsView } from "./views/settings.js";
|
|
48
|
+
import { renderStructureView } from "./views/structure.js";
|
|
49
|
+
import { highlightSql } from "./utils/format.js";
|
|
50
|
+
|
|
51
|
+
const appRoot = document.querySelector("#app");
|
|
52
|
+
|
|
53
|
+
appRoot.innerHTML = renderAppShell();
|
|
54
|
+
|
|
55
|
+
const shellRefs = {
|
|
56
|
+
shell: document.querySelector(".app-shell"),
|
|
57
|
+
topNav: document.querySelector("#top-nav"),
|
|
58
|
+
sidebar: document.querySelector("#sidebar"),
|
|
59
|
+
view: document.querySelector("#app-view"),
|
|
60
|
+
panel: document.querySelector("#app-panel"),
|
|
61
|
+
statusBar: document.querySelector("#status-bar"),
|
|
62
|
+
modal: document.querySelector("#modal-root"),
|
|
63
|
+
toast: document.querySelector("#toast-root"),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function renderQueryHighlightMarkup(query) {
|
|
67
|
+
if (query) {
|
|
68
|
+
return highlightSql(query);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return '<span class="text-on-surface-variant/35">SELECT name FROM sqlite_master WHERE type = \'table\';</span>';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function syncQueryEditorHighlight(textarea) {
|
|
75
|
+
if (!(textarea instanceof HTMLTextAreaElement)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const layer = textarea.closest(".query-editor-layer");
|
|
80
|
+
const highlightNode = layer?.querySelector("[data-query-editor-highlight]");
|
|
81
|
+
|
|
82
|
+
if (!(highlightNode instanceof HTMLElement)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
highlightNode.innerHTML = renderQueryHighlightMarkup(textarea.value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function syncQueryEditorScroll(textarea) {
|
|
90
|
+
if (!(textarea instanceof HTMLTextAreaElement)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const layer = textarea.closest(".query-editor-layer");
|
|
95
|
+
const highlightNode = layer?.querySelector("[data-query-editor-highlight]");
|
|
96
|
+
|
|
97
|
+
if (!(highlightNode instanceof HTMLElement)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
highlightNode.style.transform = `translate(${-textarea.scrollLeft}px, ${-textarea.scrollTop}px)`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function renderNotFoundView() {
|
|
105
|
+
return {
|
|
106
|
+
main: `
|
|
107
|
+
<section class="landing-view machined-grid px-6">
|
|
108
|
+
<div class="text-center z-10">
|
|
109
|
+
<p class="font-mono text-[10px] uppercase tracking-[0.3em] text-primary-container/40">
|
|
110
|
+
ROUTE_LOST // HASH_NOT_RECOGNIZED
|
|
111
|
+
</p>
|
|
112
|
+
<h1 class="mt-4 font-headline text-6xl font-black uppercase tracking-tight text-primary-container">
|
|
113
|
+
404_SIGNAL
|
|
114
|
+
</h1>
|
|
115
|
+
<button
|
|
116
|
+
class="mt-8 bg-primary-container px-6 py-3 font-headline text-sm font-bold uppercase tracking-[0.2em] text-on-primary"
|
|
117
|
+
data-action="navigate"
|
|
118
|
+
data-to="/"
|
|
119
|
+
type="button"
|
|
120
|
+
>
|
|
121
|
+
Return_Home
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
</section>
|
|
125
|
+
`,
|
|
126
|
+
panel: "",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function resolveView(state) {
|
|
131
|
+
switch (state.route.name) {
|
|
132
|
+
case "landing":
|
|
133
|
+
return renderLandingView(state);
|
|
134
|
+
case "connections":
|
|
135
|
+
return renderConnectionsView(state);
|
|
136
|
+
case "overview":
|
|
137
|
+
return renderOverviewView(state);
|
|
138
|
+
case "data":
|
|
139
|
+
return renderDataView(state);
|
|
140
|
+
case "editor":
|
|
141
|
+
return renderEditorView(state, { isResultsRoute: false });
|
|
142
|
+
case "editorResults":
|
|
143
|
+
return renderEditorView(state, { isResultsRoute: true });
|
|
144
|
+
case "structure":
|
|
145
|
+
return renderStructureView(state);
|
|
146
|
+
case "settings":
|
|
147
|
+
return renderSettingsView(state);
|
|
148
|
+
default:
|
|
149
|
+
return renderNotFoundView();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function captureFocusedInputState() {
|
|
154
|
+
const activeElement = document.activeElement;
|
|
155
|
+
|
|
156
|
+
if (
|
|
157
|
+
!activeElement ||
|
|
158
|
+
!(activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement)
|
|
159
|
+
) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const { bind } = activeElement.dataset;
|
|
164
|
+
if (!bind) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
bind,
|
|
170
|
+
selectionStart: activeElement.selectionStart,
|
|
171
|
+
selectionEnd: activeElement.selectionEnd,
|
|
172
|
+
selectionDirection: activeElement.selectionDirection,
|
|
173
|
+
scrollTop: activeElement.scrollTop,
|
|
174
|
+
scrollLeft: activeElement.scrollLeft,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function restoreFocusedInputState(snapshot) {
|
|
179
|
+
if (!snapshot) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const selector = `[data-bind="${CSS.escape(snapshot.bind)}"]`;
|
|
184
|
+
const nextElement = document.querySelector(selector);
|
|
185
|
+
|
|
186
|
+
if (
|
|
187
|
+
!nextElement ||
|
|
188
|
+
!(nextElement instanceof HTMLInputElement || nextElement instanceof HTMLTextAreaElement)
|
|
189
|
+
) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
nextElement.focus({ preventScroll: true });
|
|
194
|
+
|
|
195
|
+
if (
|
|
196
|
+
typeof snapshot.selectionStart === "number" &&
|
|
197
|
+
typeof snapshot.selectionEnd === "number"
|
|
198
|
+
) {
|
|
199
|
+
nextElement.setSelectionRange(
|
|
200
|
+
snapshot.selectionStart,
|
|
201
|
+
snapshot.selectionEnd,
|
|
202
|
+
snapshot.selectionDirection || "none"
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
nextElement.scrollTop = snapshot.scrollTop;
|
|
207
|
+
nextElement.scrollLeft = snapshot.scrollLeft;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function renderApp(state) {
|
|
211
|
+
const focusedInput = captureFocusedInputState();
|
|
212
|
+
const { main, panel } = resolveView(state);
|
|
213
|
+
const isLockedRoute = ["editor", "editorResults", "data"].includes(state.route.name);
|
|
214
|
+
|
|
215
|
+
shellRefs.topNav.innerHTML = renderTopNav(state);
|
|
216
|
+
shellRefs.sidebar.innerHTML = renderSidebar(state);
|
|
217
|
+
shellRefs.statusBar.innerHTML = renderStatusBar(state);
|
|
218
|
+
shellRefs.view.innerHTML = main;
|
|
219
|
+
shellRefs.view.classList.toggle("app-main-scroll--locked", isLockedRoute);
|
|
220
|
+
shellRefs.panel.innerHTML = panel;
|
|
221
|
+
shellRefs.modal.innerHTML = renderModal(state);
|
|
222
|
+
shellRefs.toast.innerHTML = renderToasts(state.toasts);
|
|
223
|
+
shellRefs.shell.classList.toggle("panel-open", Boolean(panel));
|
|
224
|
+
restoreFocusedInputState(focusedInput);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const router = createRouter((route) => {
|
|
228
|
+
setRoute(route);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
async function handleAction(actionNode) {
|
|
232
|
+
const { action } = actionNode.dataset;
|
|
233
|
+
|
|
234
|
+
switch (action) {
|
|
235
|
+
case "navigate":
|
|
236
|
+
router.navigate(actionNode.dataset.to ?? "/");
|
|
237
|
+
return;
|
|
238
|
+
case "refresh-view":
|
|
239
|
+
await refreshCurrentRoute();
|
|
240
|
+
return;
|
|
241
|
+
case "open-modal":
|
|
242
|
+
openModal(actionNode.dataset.modal);
|
|
243
|
+
return;
|
|
244
|
+
case "edit-connection":
|
|
245
|
+
openEditConnectionModal(actionNode.dataset.connectionId);
|
|
246
|
+
return;
|
|
247
|
+
case "close-modal":
|
|
248
|
+
closeModal();
|
|
249
|
+
return;
|
|
250
|
+
case "dismiss-toast":
|
|
251
|
+
dismissToast(actionNode.dataset.toastId);
|
|
252
|
+
return;
|
|
253
|
+
case "select-connection": {
|
|
254
|
+
const next = await selectConnection(actionNode.dataset.connectionId);
|
|
255
|
+
if (next) {
|
|
256
|
+
router.navigate("/overview");
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
case "remove-connection": {
|
|
261
|
+
const removed = await removeConnection(actionNode.dataset.connectionId);
|
|
262
|
+
if (removed) {
|
|
263
|
+
const nextState = getState();
|
|
264
|
+
if (!nextState.connections.active && nextState.route.name !== "connections") {
|
|
265
|
+
router.navigate("/connections");
|
|
266
|
+
} else {
|
|
267
|
+
await refreshCurrentRoute();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
case "execute-query": {
|
|
273
|
+
const success = await executeCurrentQuery();
|
|
274
|
+
router.navigate(success ? "/editor/results" : "/editor");
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
case "clear-query":
|
|
278
|
+
clearCurrentQuery();
|
|
279
|
+
return;
|
|
280
|
+
case "clear-results":
|
|
281
|
+
clearEditorResults();
|
|
282
|
+
router.navigate("/editor");
|
|
283
|
+
return;
|
|
284
|
+
case "set-editor-tab": {
|
|
285
|
+
const tab = actionNode.dataset.tab;
|
|
286
|
+
if (!tab) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
setEditorTab(tab);
|
|
290
|
+
router.navigate(tab === "results" ? "/editor/results" : "/editor");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
case "clear-sql-history":
|
|
294
|
+
await clearSqlHistoryStateAndData();
|
|
295
|
+
return;
|
|
296
|
+
case "export-query-csv":
|
|
297
|
+
await exportCurrentQueryCsv();
|
|
298
|
+
return;
|
|
299
|
+
case "select-structure-entry":
|
|
300
|
+
if (actionNode.dataset.entryName) {
|
|
301
|
+
await selectStructureEntry(actionNode.dataset.entryName);
|
|
302
|
+
}
|
|
303
|
+
return;
|
|
304
|
+
case "select-data-row":
|
|
305
|
+
if (actionNode.dataset.rowIndex) {
|
|
306
|
+
selectDataRow(actionNode.dataset.rowIndex);
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
case "select-editor-row":
|
|
310
|
+
if (actionNode.dataset.rowIndex) {
|
|
311
|
+
selectEditorRow(actionNode.dataset.rowIndex);
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
case "clear-data-row-selection":
|
|
315
|
+
clearDataRowSelection();
|
|
316
|
+
return;
|
|
317
|
+
case "clear-editor-row-selection":
|
|
318
|
+
clearEditorRowSelection();
|
|
319
|
+
return;
|
|
320
|
+
case "set-data-page":
|
|
321
|
+
if (actionNode.dataset.page) {
|
|
322
|
+
await setDataPage(actionNode.dataset.page);
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
case "set-data-page-size":
|
|
326
|
+
if (actionNode.dataset.pageSize) {
|
|
327
|
+
await setDataPageSize(actionNode.dataset.pageSize);
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
case "reload-data-route":
|
|
331
|
+
await refreshCurrentRoute();
|
|
332
|
+
return;
|
|
333
|
+
default:
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
document.addEventListener("click", (event) => {
|
|
338
|
+
const actionNode = event.target.closest("[data-action]");
|
|
339
|
+
|
|
340
|
+
if (!actionNode) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
handleAction(actionNode);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
document.addEventListener("keydown", (event) => {
|
|
348
|
+
if (event.key !== "Escape" || event.defaultPrevented) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const state = getState();
|
|
353
|
+
|
|
354
|
+
if (state.modal) {
|
|
355
|
+
event.preventDefault();
|
|
356
|
+
closeModal();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (state.route.name === "data" && typeof state.dataBrowser.selectedRowIndex === "number") {
|
|
361
|
+
event.preventDefault();
|
|
362
|
+
clearDataRowSelection();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (state.route.name === "editorResults" && typeof state.editor.selectedRowIndex === "number") {
|
|
367
|
+
event.preventDefault();
|
|
368
|
+
clearEditorRowSelection();
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
document.addEventListener("input", (event) => {
|
|
373
|
+
const bindNode = event.target.closest("[data-bind]");
|
|
374
|
+
|
|
375
|
+
if (!bindNode) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (bindNode.dataset.bind === "current-query") {
|
|
380
|
+
syncQueryEditorHighlight(bindNode);
|
|
381
|
+
syncQueryEditorScroll(bindNode);
|
|
382
|
+
setCurrentQuery(bindNode.value);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
document.addEventListener(
|
|
387
|
+
"scroll",
|
|
388
|
+
(event) => {
|
|
389
|
+
const target = event.target;
|
|
390
|
+
|
|
391
|
+
if (!(target instanceof HTMLTextAreaElement) || target.dataset.bind !== "current-query") {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
syncQueryEditorScroll(target);
|
|
396
|
+
},
|
|
397
|
+
true
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
document.addEventListener("change", (event) => {
|
|
401
|
+
const bindNode = event.target.closest("[data-bind]");
|
|
402
|
+
|
|
403
|
+
if (!bindNode) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (bindNode.dataset.bind === "history-entry" && bindNode.value) {
|
|
408
|
+
loadQueryFromHistory(bindNode.value);
|
|
409
|
+
bindNode.value = "";
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
document.addEventListener("submit", async (event) => {
|
|
414
|
+
const form = event.target.closest("[data-form]");
|
|
415
|
+
|
|
416
|
+
if (!form) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
event.preventDefault();
|
|
421
|
+
const formData = new FormData(form);
|
|
422
|
+
|
|
423
|
+
switch (form.dataset.form) {
|
|
424
|
+
case "open-connection": {
|
|
425
|
+
const connection = await submitOpenConnection({
|
|
426
|
+
path: String(formData.get("path") ?? ""),
|
|
427
|
+
label: String(formData.get("label") ?? ""),
|
|
428
|
+
readOnly: formData.get("readOnly") === "on",
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
if (connection) {
|
|
432
|
+
router.navigate("/overview");
|
|
433
|
+
}
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
case "create-connection": {
|
|
437
|
+
const connection = await submitCreateConnection({
|
|
438
|
+
path: String(formData.get("path") ?? ""),
|
|
439
|
+
label: String(formData.get("label") ?? ""),
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
if (connection) {
|
|
443
|
+
router.navigate("/overview");
|
|
444
|
+
}
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
case "import-sql": {
|
|
448
|
+
const targetMode = String(formData.get("targetMode") ?? "active");
|
|
449
|
+
const payload = {
|
|
450
|
+
sqlFilePath: String(formData.get("sqlFilePath") ?? ""),
|
|
451
|
+
label: String(formData.get("label") ?? ""),
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
if (targetMode === "recent") {
|
|
455
|
+
payload.targetConnectionId = String(formData.get("targetConnectionId") ?? "");
|
|
456
|
+
} else if (targetMode === "create") {
|
|
457
|
+
payload.createNew = true;
|
|
458
|
+
payload.targetPath = String(formData.get("targetPath") ?? "");
|
|
459
|
+
} else if (targetMode === "path") {
|
|
460
|
+
payload.targetPath = String(formData.get("targetPath") ?? "");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const result = await submitImportSql(payload);
|
|
464
|
+
if (result) {
|
|
465
|
+
router.navigate("/overview");
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
case "edit-connection": {
|
|
470
|
+
await submitEditConnection(String(formData.get("connectionId") ?? ""), {
|
|
471
|
+
path: String(formData.get("path") ?? ""),
|
|
472
|
+
label: String(formData.get("label") ?? ""),
|
|
473
|
+
readOnly: formData.get("readOnly") === "on",
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
case "save-data-row": {
|
|
479
|
+
const values = {};
|
|
480
|
+
|
|
481
|
+
for (const [key, value] of formData.entries()) {
|
|
482
|
+
if (!key.startsWith("field:")) {
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
values[key.slice("field:".length)] = String(value ?? "");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
await submitDataRowUpdate(
|
|
490
|
+
String(formData.get("rowIndex") ?? ""),
|
|
491
|
+
values
|
|
492
|
+
);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
case "save-editor-row": {
|
|
496
|
+
const values = {};
|
|
497
|
+
|
|
498
|
+
for (const [key, value] of formData.entries()) {
|
|
499
|
+
if (!key.startsWith("field:")) {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
values[key.slice("field:".length)] = String(value ?? "");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
await submitEditorRowUpdate(
|
|
507
|
+
String(formData.get("rowIndex") ?? ""),
|
|
508
|
+
values
|
|
509
|
+
);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
default:
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
subscribe(renderApp);
|
|
517
|
+
renderApp(getState());
|
|
518
|
+
initializeApp().then(() => {
|
|
519
|
+
router.start();
|
|
520
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function renderActionBar({ left = "", right = "", className = "" }) {
|
|
2
|
+
return `
|
|
3
|
+
<div class="flex items-center justify-between gap-4 ${className}">
|
|
4
|
+
<div class="flex items-center gap-4">${left}</div>
|
|
5
|
+
<div class="flex items-center gap-3">${right}</div>
|
|
6
|
+
</div>
|
|
7
|
+
`;
|
|
8
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function renderAppShell() {
|
|
2
|
+
return `
|
|
3
|
+
<div class="app-shell">
|
|
4
|
+
<header id="top-nav" class="app-top-nav bg-[#131313]"></header>
|
|
5
|
+
<div class="app-body">
|
|
6
|
+
<aside id="sidebar" class="app-sidebar sidebar-shell"></aside>
|
|
7
|
+
<main class="app-main">
|
|
8
|
+
<div id="app-view" class="app-main-scroll app-view custom-scrollbar"></div>
|
|
9
|
+
</main>
|
|
10
|
+
<aside id="app-panel" class="app-right-panel"></aside>
|
|
11
|
+
</div>
|
|
12
|
+
<footer id="status-bar" class="app-status-bar"></footer>
|
|
13
|
+
</div>
|
|
14
|
+
<div id="modal-root"></div>
|
|
15
|
+
<div id="toast-root"></div>
|
|
16
|
+
`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { escapeHtml } from "../utils/format.js";
|
|
2
|
+
|
|
3
|
+
export function renderBottomTabs(activeTab, counts = {}) {
|
|
4
|
+
const tabs = [
|
|
5
|
+
{ key: "results", label: "results", meta: counts.resultRows ?? 0 },
|
|
6
|
+
{ key: "messages", label: "messages", meta: counts.messages ?? 0 },
|
|
7
|
+
{ key: "performance", label: "performance", meta: counts.statementCount ?? 0 },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
return `
|
|
11
|
+
<div class="flex items-center justify-between border-b border-outline-variant/5 bg-surface-container-low/50 px-4 h-10">
|
|
12
|
+
<div class="flex items-center gap-6">
|
|
13
|
+
${tabs
|
|
14
|
+
.map(
|
|
15
|
+
(tab) => `
|
|
16
|
+
<button
|
|
17
|
+
class="bottom-tab ${activeTab === tab.key ? "is-active" : ""}"
|
|
18
|
+
data-action="set-editor-tab"
|
|
19
|
+
data-tab="${tab.key}"
|
|
20
|
+
type="button"
|
|
21
|
+
>
|
|
22
|
+
${tab.label}
|
|
23
|
+
<span class="ml-2 text-[9px] opacity-50">${escapeHtml(String(tab.meta))}</span>
|
|
24
|
+
</button>
|
|
25
|
+
`
|
|
26
|
+
)
|
|
27
|
+
.join("")}
|
|
28
|
+
</div>
|
|
29
|
+
<div class="flex items-center gap-2">
|
|
30
|
+
<span class="material-symbols-outlined text-xs text-on-surface-variant/30">database</span>
|
|
31
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/45">
|
|
32
|
+
sqlite
|
|
33
|
+
</span>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {
|
|
2
|
+
escapeHtml,
|
|
3
|
+
formatBytes,
|
|
4
|
+
formatDateTime,
|
|
5
|
+
truncateMiddle,
|
|
6
|
+
} from "../utils/format.js";
|
|
7
|
+
import { renderStatusBadge } from "./badges.js";
|
|
8
|
+
|
|
9
|
+
export function renderConnectionCard(connection, activeConnectionId) {
|
|
10
|
+
const isActive = activeConnectionId
|
|
11
|
+
? connection.id === activeConnectionId
|
|
12
|
+
: Boolean(connection.isActive);
|
|
13
|
+
const clipPath = "polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%)";
|
|
14
|
+
const primaryActionLabel = isActive ? "Open Overview" : "Set Active";
|
|
15
|
+
|
|
16
|
+
return `
|
|
17
|
+
<article
|
|
18
|
+
class="connection-card clipped-corner ${isActive ? "is-active" : ""}"
|
|
19
|
+
style="--clip-path: ${clipPath};"
|
|
20
|
+
>
|
|
21
|
+
<div class="flex-1 p-6">
|
|
22
|
+
<div class="mb-6 flex items-start justify-between">
|
|
23
|
+
<div
|
|
24
|
+
class="clipped-corner flex h-10 w-10 items-center justify-center transition-colors ${
|
|
25
|
+
isActive ? "bg-primary-container" : "bg-surface-container-highest"
|
|
26
|
+
}"
|
|
27
|
+
style="--clip-path: ${clipPath};"
|
|
28
|
+
>
|
|
29
|
+
<span class="material-symbols-outlined ${
|
|
30
|
+
isActive ? "text-on-primary" : "text-outline-variant"
|
|
31
|
+
}">database</span>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="flex items-center gap-2">
|
|
34
|
+
${renderStatusBadge(isActive ? "ACTIVE" : "RECENT", isActive ? "primary" : "muted")}
|
|
35
|
+
${connection.readOnly ? renderStatusBadge("READ_ONLY", "alert") : ""}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<h3 class="mb-1 font-headline text-xl font-bold uppercase ${
|
|
39
|
+
isActive ? "text-[#FCE300]" : "text-on-surface"
|
|
40
|
+
}">
|
|
41
|
+
${escapeHtml(connection.label)}
|
|
42
|
+
</h3>
|
|
43
|
+
<p
|
|
44
|
+
class="block overflow-hidden text-ellipsis whitespace-nowrap font-mono text-[10px] text-outline-variant"
|
|
45
|
+
title="${escapeHtml(connection.path)}"
|
|
46
|
+
>
|
|
47
|
+
${escapeHtml(truncateMiddle(connection.path, 68))}
|
|
48
|
+
</p>
|
|
49
|
+
<div class="mt-8 grid grid-cols-2 gap-4">
|
|
50
|
+
<div>
|
|
51
|
+
<div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Allocation</div>
|
|
52
|
+
<div class="text-xs font-bold text-on-surface">${escapeHtml(
|
|
53
|
+
formatBytes(connection.sizeBytes)
|
|
54
|
+
)}</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div>
|
|
57
|
+
<div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Last Modified</div>
|
|
58
|
+
<div class="text-xs font-bold text-on-surface">${escapeHtml(
|
|
59
|
+
formatDateTime(connection.lastModifiedAt)
|
|
60
|
+
)}</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div>
|
|
63
|
+
<div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Last Opened</div>
|
|
64
|
+
<div class="text-xs font-bold text-on-surface">${escapeHtml(
|
|
65
|
+
formatDateTime(connection.lastOpenedAt)
|
|
66
|
+
)}</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div>
|
|
69
|
+
<div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Mode</div>
|
|
70
|
+
<div class="text-xs font-bold text-on-surface">${connection.readOnly ? "Read only" : "Read / Write"}</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="border-t border-outline-variant/10 bg-surface-container-low px-4 py-3">
|
|
75
|
+
<div class="grid grid-cols-[minmax(0,1fr)_5.1rem_5.8rem] gap-2">
|
|
76
|
+
<button
|
|
77
|
+
class="clipped-corner min-w-0 bg-primary-container px-3 py-2.5 text-[10px] font-black uppercase tracking-[0.18em] text-on-primary shadow-[0_0_18px_-10px_rgba(252,227,0,0.7)] transition-[filter,box-shadow] hover:brightness-105"
|
|
78
|
+
data-action="select-connection"
|
|
79
|
+
data-connection-id="${escapeHtml(connection.id)}"
|
|
80
|
+
style="--clip-path: ${clipPath};"
|
|
81
|
+
type="button"
|
|
82
|
+
title="${primaryActionLabel}"
|
|
83
|
+
>
|
|
84
|
+
${primaryActionLabel}
|
|
85
|
+
</button>
|
|
86
|
+
<button
|
|
87
|
+
class="border border-outline-variant/20 bg-surface-container px-3 py-2.5 text-[10px] font-bold uppercase tracking-[0.18em] text-on-surface-variant transition-colors hover:bg-surface-container-highest"
|
|
88
|
+
data-action="edit-connection"
|
|
89
|
+
data-connection-id="${escapeHtml(connection.id)}"
|
|
90
|
+
type="button"
|
|
91
|
+
>
|
|
92
|
+
Edit
|
|
93
|
+
</button>
|
|
94
|
+
<button
|
|
95
|
+
class="border border-outline-variant/20 bg-surface-container px-3 py-2.5 text-[10px] font-bold uppercase tracking-[0.18em] text-on-surface-variant transition-colors hover:bg-surface-container-highest"
|
|
96
|
+
data-action="remove-connection"
|
|
97
|
+
data-connection-id="${escapeHtml(connection.id)}"
|
|
98
|
+
type="button"
|
|
99
|
+
>
|
|
100
|
+
Remove
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</article>
|
|
105
|
+
`;
|
|
106
|
+
}
|