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/views/data.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { renderDataGrid } from "../components/dataGrid.js";
|
|
2
|
+
import { renderRowEditorPanel } from "../components/rowEditorPanel.js";
|
|
3
|
+
import {
|
|
4
|
+
escapeHtml,
|
|
5
|
+
formatCellValue,
|
|
6
|
+
formatNumber,
|
|
7
|
+
isBlobPreview,
|
|
8
|
+
truncateMiddle,
|
|
9
|
+
} from "../utils/format.js";
|
|
10
|
+
|
|
11
|
+
function getSelectedRow(state) {
|
|
12
|
+
const rowIndex = state.dataBrowser.selectedRowIndex;
|
|
13
|
+
|
|
14
|
+
if (typeof rowIndex !== "number") {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return state.dataBrowser.table?.rows?.[rowIndex] ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function renderTableList(state) {
|
|
22
|
+
const tables = state.dataBrowser.tables ?? [];
|
|
23
|
+
const activeName = state.dataBrowser.selectedTable;
|
|
24
|
+
|
|
25
|
+
if (state.dataBrowser.loading && !tables.length) {
|
|
26
|
+
return `
|
|
27
|
+
<div class="flex flex-1 items-center justify-center px-6">
|
|
28
|
+
<div class="text-center text-on-surface-variant/40">
|
|
29
|
+
<span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
|
|
30
|
+
<p class="font-mono text-[10px] uppercase tracking-[0.22em]">LOADING_TABLES</p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!tables.length) {
|
|
37
|
+
return `
|
|
38
|
+
<div class="px-6 py-6 text-sm text-on-surface-variant/55">
|
|
39
|
+
No tables found in the active SQLite database.
|
|
40
|
+
</div>
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return `
|
|
45
|
+
<div class="custom-scrollbar flex-1 overflow-auto px-4 py-4">
|
|
46
|
+
<div class="space-y-2">
|
|
47
|
+
${tables
|
|
48
|
+
.map(
|
|
49
|
+
(table) => `
|
|
50
|
+
<button
|
|
51
|
+
class="w-full border px-4 py-3 text-left transition-colors ${
|
|
52
|
+
table.name === activeName
|
|
53
|
+
? "border-primary-container/30 bg-surface-container-high"
|
|
54
|
+
: "border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high"
|
|
55
|
+
}"
|
|
56
|
+
data-action="navigate"
|
|
57
|
+
data-to="/data/${encodeURIComponent(table.name)}"
|
|
58
|
+
type="button"
|
|
59
|
+
>
|
|
60
|
+
<div class="truncate font-mono text-xs ${
|
|
61
|
+
table.name === activeName ? "text-primary-container" : "text-on-surface"
|
|
62
|
+
}">
|
|
63
|
+
${escapeHtml(table.name)}
|
|
64
|
+
</div>
|
|
65
|
+
</button>
|
|
66
|
+
`
|
|
67
|
+
)
|
|
68
|
+
.join("")}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renderWorkspaceHeader(state) {
|
|
75
|
+
const table = state.dataBrowser.table;
|
|
76
|
+
|
|
77
|
+
return `
|
|
78
|
+
<header class="border-b border-outline-variant/10 bg-surface-container px-6 py-5">
|
|
79
|
+
<div class="flex flex-wrap items-end justify-between gap-4">
|
|
80
|
+
<div>
|
|
81
|
+
<div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
|
|
82
|
+
Data Browser
|
|
83
|
+
</div>
|
|
84
|
+
<h1 class="mt-2 font-headline text-4xl font-black uppercase tracking-tight text-primary-container">
|
|
85
|
+
${escapeHtml(table?.name ?? "Table Data")}
|
|
86
|
+
</h1>
|
|
87
|
+
<div class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
88
|
+
${
|
|
89
|
+
table
|
|
90
|
+
? `rows ${escapeHtml(formatNumber(table.rowCount ?? 0))} // columns ${escapeHtml(
|
|
91
|
+
formatNumber(table.columns?.length ?? 0)
|
|
92
|
+
)}`
|
|
93
|
+
: `tables ${escapeHtml(formatNumber(state.dataBrowser.tables.length))}`
|
|
94
|
+
}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="flex items-center gap-3">
|
|
98
|
+
<button
|
|
99
|
+
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"
|
|
100
|
+
data-action="refresh-view"
|
|
101
|
+
type="button"
|
|
102
|
+
>
|
|
103
|
+
Reload Data
|
|
104
|
+
</button>
|
|
105
|
+
${
|
|
106
|
+
table
|
|
107
|
+
? `
|
|
108
|
+
<button
|
|
109
|
+
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"
|
|
110
|
+
data-action="navigate"
|
|
111
|
+
data-to="/structure"
|
|
112
|
+
type="button"
|
|
113
|
+
>
|
|
114
|
+
Open Structure
|
|
115
|
+
</button>
|
|
116
|
+
`
|
|
117
|
+
: ""
|
|
118
|
+
}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</header>
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function renderWorkspaceError(state) {
|
|
126
|
+
if (!state.dataBrowser.error) {
|
|
127
|
+
return "";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return `
|
|
131
|
+
<div class="border-b border-error/20 bg-error-container/10 px-6 py-4 text-sm text-on-surface">
|
|
132
|
+
<div class="font-headline text-xs font-bold uppercase tracking-[0.18em] text-error">
|
|
133
|
+
${escapeHtml(state.dataBrowser.error.code)}
|
|
134
|
+
</div>
|
|
135
|
+
<div class="mt-2">${escapeHtml(state.dataBrowser.error.message)}</div>
|
|
136
|
+
</div>
|
|
137
|
+
`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getCellWidthClass(columnName) {
|
|
141
|
+
const normalized = String(columnName ?? "").toLowerCase();
|
|
142
|
+
|
|
143
|
+
if (/(path|url|hash|sql|query|content|description|message|title|name)/.test(normalized)) {
|
|
144
|
+
return "max-w-[18rem]";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (/(date|time|modified|created|updated|timestamp)/.test(normalized)) {
|
|
148
|
+
return "max-w-[11rem]";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (/(id|uuid|token|key)/.test(normalized)) {
|
|
152
|
+
return "max-w-[10rem]";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return "max-w-[12rem]";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function renderTableSurface(state) {
|
|
159
|
+
const table = state.dataBrowser.table;
|
|
160
|
+
|
|
161
|
+
if (state.dataBrowser.tableLoading && !table) {
|
|
162
|
+
return `
|
|
163
|
+
<div class="flex flex-1 items-center justify-center">
|
|
164
|
+
<div class="text-center text-on-surface-variant/40">
|
|
165
|
+
<span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
|
|
166
|
+
<p class="font-mono text-[10px] uppercase tracking-[0.22em]">LOADING_TABLE_DATA</p>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!table) {
|
|
173
|
+
return `
|
|
174
|
+
<div class="flex flex-1 items-center justify-center px-8 text-center">
|
|
175
|
+
<div>
|
|
176
|
+
<div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
|
|
177
|
+
Full Table
|
|
178
|
+
</div>
|
|
179
|
+
<p class="mt-3 text-sm text-on-surface-variant/55">
|
|
180
|
+
Select a table to browse and edit its rows.
|
|
181
|
+
</p>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const columns = (table.columns ?? []).map((columnName) => ({
|
|
188
|
+
label: escapeHtml(columnName),
|
|
189
|
+
headerClassName:
|
|
190
|
+
"border-b border-primary-container/20 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-primary-container",
|
|
191
|
+
cellClassName: "px-4 py-2 align-top text-[11px] text-on-surface",
|
|
192
|
+
render: (row) => {
|
|
193
|
+
const value = formatCellValue(row[columnName]);
|
|
194
|
+
const isNull = value === "NULL";
|
|
195
|
+
const widthClass = getCellWidthClass(columnName);
|
|
196
|
+
const displayValue = isNull ? value : truncateMiddle(value, 48);
|
|
197
|
+
|
|
198
|
+
return `<span class="block ${widthClass} overflow-hidden text-ellipsis whitespace-nowrap ${
|
|
199
|
+
isNull ? "text-on-surface-variant/45" : "text-on-surface"
|
|
200
|
+
}" title="${escapeHtml(value)}">${escapeHtml(displayValue)}</span>`;
|
|
201
|
+
},
|
|
202
|
+
}));
|
|
203
|
+
const totalRows = table.rowCount ?? 0;
|
|
204
|
+
const page = table.page ?? state.dataBrowser.page ?? 1;
|
|
205
|
+
const pageCount = table.pageCount ?? Math.max(1, Math.ceil(totalRows / (table.limit ?? 50)));
|
|
206
|
+
const fromRow = totalRows === 0 ? 0 : (table.offset ?? 0) + 1;
|
|
207
|
+
const toRow = totalRows === 0 ? 0 : Math.min((table.offset ?? 0) + (table.rows?.length ?? 0), totalRows);
|
|
208
|
+
const pageSizes = [25, 50, 100];
|
|
209
|
+
|
|
210
|
+
return `
|
|
211
|
+
<div class="flex flex-1 min-h-0 flex-col bg-surface-container-lowest">
|
|
212
|
+
<div class="custom-scrollbar flex-1 overflow-auto">
|
|
213
|
+
${renderDataGrid({
|
|
214
|
+
columns,
|
|
215
|
+
rows: table.rows ?? [],
|
|
216
|
+
tableClass: "min-w-full border-collapse text-left font-mono text-xs",
|
|
217
|
+
theadClass: "sticky top-0 z-10 bg-surface-container-highest",
|
|
218
|
+
tbodyClass: "divide-y divide-outline-variant/5",
|
|
219
|
+
getRowClass: (_, index) =>
|
|
220
|
+
`${
|
|
221
|
+
state.dataBrowser.selectedRowIndex === index
|
|
222
|
+
? "bg-surface-bright"
|
|
223
|
+
: index % 2 === 0
|
|
224
|
+
? "bg-surface-container-low"
|
|
225
|
+
: "bg-surface-container-lowest"
|
|
226
|
+
} cursor-pointer transition-colors hover:bg-surface-container-high`,
|
|
227
|
+
getRowAttrs: (_, index) => `data-action="select-data-row" data-row-index="${index}"`,
|
|
228
|
+
})}
|
|
229
|
+
${
|
|
230
|
+
!table.rows?.length
|
|
231
|
+
? `
|
|
232
|
+
<div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10">
|
|
233
|
+
<p class="font-mono text-[10px] uppercase tracking-[0.22em] text-on-surface-variant/40">
|
|
234
|
+
TABLE_IS_EMPTY
|
|
235
|
+
</p>
|
|
236
|
+
</div>
|
|
237
|
+
`
|
|
238
|
+
: ""
|
|
239
|
+
}
|
|
240
|
+
</div>
|
|
241
|
+
<footer class="flex flex-wrap items-center justify-between gap-4 border-t border-outline-variant/10 bg-surface-container px-6 py-4">
|
|
242
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
243
|
+
showing ${escapeHtml(formatNumber(fromRow))}-${escapeHtml(formatNumber(toRow))} of ${escapeHtml(
|
|
244
|
+
formatNumber(totalRows)
|
|
245
|
+
)} rows
|
|
246
|
+
</div>
|
|
247
|
+
<div class="flex flex-wrap items-center gap-4">
|
|
248
|
+
<div class="flex items-center gap-2">
|
|
249
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
250
|
+
rows
|
|
251
|
+
</span>
|
|
252
|
+
<div class="flex items-center gap-2">
|
|
253
|
+
${pageSizes
|
|
254
|
+
.map(
|
|
255
|
+
(pageSize) => `
|
|
256
|
+
<button
|
|
257
|
+
class="border px-3 py-2 text-[10px] font-bold uppercase tracking-[0.16em] transition-colors ${
|
|
258
|
+
pageSize === (table.limit ?? state.dataBrowser.pageSize)
|
|
259
|
+
? "border-primary-container/30 bg-surface-container-high text-primary-container"
|
|
260
|
+
: "border-outline-variant/20 text-on-surface hover:bg-surface-container-highest"
|
|
261
|
+
}"
|
|
262
|
+
data-action="set-data-page-size"
|
|
263
|
+
data-page-size="${pageSize}"
|
|
264
|
+
type="button"
|
|
265
|
+
>
|
|
266
|
+
${pageSize}
|
|
267
|
+
</button>
|
|
268
|
+
`
|
|
269
|
+
)
|
|
270
|
+
.join("")}
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
<div class="flex items-center gap-2">
|
|
274
|
+
<button
|
|
275
|
+
class="border border-outline-variant/20 px-3 py-2 text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface transition-colors hover:bg-surface-container-highest disabled:cursor-default disabled:opacity-30"
|
|
276
|
+
data-action="set-data-page"
|
|
277
|
+
data-page="${page - 1}"
|
|
278
|
+
type="button"
|
|
279
|
+
${page <= 1 ? "disabled" : ""}
|
|
280
|
+
>
|
|
281
|
+
Prev
|
|
282
|
+
</button>
|
|
283
|
+
<div class="min-w-[7rem] text-center text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
284
|
+
page ${escapeHtml(formatNumber(page))} / ${escapeHtml(formatNumber(pageCount))}
|
|
285
|
+
</div>
|
|
286
|
+
<button
|
|
287
|
+
class="border border-outline-variant/20 px-3 py-2 text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface transition-colors hover:bg-surface-container-highest disabled:cursor-default disabled:opacity-30"
|
|
288
|
+
data-action="set-data-page"
|
|
289
|
+
data-page="${page + 1}"
|
|
290
|
+
type="button"
|
|
291
|
+
${page >= pageCount ? "disabled" : ""}
|
|
292
|
+
>
|
|
293
|
+
Next
|
|
294
|
+
</button>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
</footer>
|
|
298
|
+
</div>
|
|
299
|
+
`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function renderDataRowEditorPanel(state) {
|
|
303
|
+
const table = state.dataBrowser.table;
|
|
304
|
+
const rowIndex = state.dataBrowser.selectedRowIndex;
|
|
305
|
+
const row = getSelectedRow(state);
|
|
306
|
+
|
|
307
|
+
if (!table || !row || typeof rowIndex !== "number") {
|
|
308
|
+
return "";
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const identityColumns =
|
|
312
|
+
table.identityStrategy?.type === "primaryKey" ? table.identityStrategy.columns ?? [] : [];
|
|
313
|
+
const editableColumns = (table.columnMeta ?? []).filter((column) => {
|
|
314
|
+
if (!column.visible || column.generated) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (identityColumns.includes(column.name)) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const value = row[column.name];
|
|
323
|
+
if (isBlobPreview(value) || (value && typeof value === "object")) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return true;
|
|
328
|
+
});
|
|
329
|
+
const readonlyColumns = (table.columnMeta ?? []).filter((column) => {
|
|
330
|
+
if (!column.visible) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (identityColumns.includes(column.name) || column.generated) {
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const value = row[column.name];
|
|
339
|
+
return isBlobPreview(value) || (value && typeof value === "object");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
return renderRowEditorPanel({
|
|
343
|
+
title: table.name,
|
|
344
|
+
sectionLabel: "Row Editor",
|
|
345
|
+
subtitle: `row ${rowIndex + 1}`,
|
|
346
|
+
closeAction: "clear-data-row-selection",
|
|
347
|
+
formName: "save-data-row",
|
|
348
|
+
hiddenFields: [{ name: "rowIndex", value: String(rowIndex) }],
|
|
349
|
+
disabledMessage: state.connections.active?.readOnly
|
|
350
|
+
? "The active database is opened read-only, so row editing is disabled."
|
|
351
|
+
: table.notSafelyUpdatable
|
|
352
|
+
? "This table has no stable identity column, so SQLite Hub cannot safely update rows."
|
|
353
|
+
: "",
|
|
354
|
+
editableFields: editableColumns.map((column) => {
|
|
355
|
+
const value = row[column.name];
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
name: column.name,
|
|
359
|
+
label: column.name,
|
|
360
|
+
value: value === null || value === undefined ? "" : String(value),
|
|
361
|
+
};
|
|
362
|
+
}),
|
|
363
|
+
readonlyFields: readonlyColumns.map((column) => ({
|
|
364
|
+
name: column.name,
|
|
365
|
+
label: column.name,
|
|
366
|
+
value: formatCellValue(row[column.name]),
|
|
367
|
+
})),
|
|
368
|
+
saveError: state.dataBrowser.saveError,
|
|
369
|
+
saving: state.dataBrowser.saving,
|
|
370
|
+
reloadAction: "reload-data-route",
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function renderDataView(state) {
|
|
375
|
+
return {
|
|
376
|
+
main: `
|
|
377
|
+
<section class="view-surface min-h-full bg-surface-container flex h-full min-h-0 flex-col overflow-hidden">
|
|
378
|
+
<div class="grid h-full min-h-0 grid-cols-1 md:grid-cols-[18rem_minmax(0,1fr)]">
|
|
379
|
+
<aside class="flex min-h-0 flex-col border-r border-outline-variant/10 bg-surface-low">
|
|
380
|
+
<div class="border-b border-outline-variant/10 px-6 py-5">
|
|
381
|
+
<div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
|
|
382
|
+
Tables
|
|
383
|
+
</div>
|
|
384
|
+
<div class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
385
|
+
total ${escapeHtml(formatNumber(state.dataBrowser.tables.length))}
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
${renderTableList(state)}
|
|
389
|
+
</aside>
|
|
390
|
+
<section class="flex min-h-0 flex-col overflow-hidden">
|
|
391
|
+
${renderWorkspaceHeader(state)}
|
|
392
|
+
${renderWorkspaceError(state)}
|
|
393
|
+
${renderTableSurface(state)}
|
|
394
|
+
</section>
|
|
395
|
+
</div>
|
|
396
|
+
</section>
|
|
397
|
+
`,
|
|
398
|
+
panel: renderDataRowEditorPanel(state),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { renderBottomTabs } from "../components/bottomTabs.js";
|
|
2
|
+
import { renderQueryEditor } from "../components/queryEditor.js";
|
|
3
|
+
import { renderRowEditorPanel } from "../components/rowEditorPanel.js";
|
|
4
|
+
import { renderQueryResultsPane } from "../components/queryResults.js";
|
|
5
|
+
import { getCurrentConnection, getQueryMessages, getQueryPerformance } from "../store.js";
|
|
6
|
+
import {
|
|
7
|
+
escapeHtml,
|
|
8
|
+
formatCellValue,
|
|
9
|
+
formatNumber,
|
|
10
|
+
isBlobPreview,
|
|
11
|
+
} from "../utils/format.js";
|
|
12
|
+
|
|
13
|
+
function renderMissingDatabase() {
|
|
14
|
+
return `
|
|
15
|
+
<div class="flex flex-1 flex-col items-center justify-center bg-surface-container-lowest px-8 text-center">
|
|
16
|
+
<span class="material-symbols-outlined mb-3 text-5xl text-on-surface-variant/25">database_off</span>
|
|
17
|
+
<p class="font-headline text-xl font-black uppercase tracking-tight text-primary-container">
|
|
18
|
+
No Active SQLite Database
|
|
19
|
+
</p>
|
|
20
|
+
<p class="mt-3 max-w-xl text-sm leading-7 text-on-surface-variant/65">
|
|
21
|
+
Connect a local SQLite database before executing statements or exporting query results.
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
24
|
+
`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function renderMessagesPane(state) {
|
|
28
|
+
const items = getQueryMessages(state);
|
|
29
|
+
|
|
30
|
+
return `
|
|
31
|
+
<div class="custom-scrollbar h-full overflow-auto bg-surface-container-lowest px-6 py-6">
|
|
32
|
+
<div class="space-y-4">
|
|
33
|
+
${items
|
|
34
|
+
.map(
|
|
35
|
+
(item) => `
|
|
36
|
+
<div class="border border-outline-variant/10 bg-surface-container-low px-4 py-4">
|
|
37
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.2em] ${
|
|
38
|
+
item.tone === "alert" ? "text-error" : "text-primary-container"
|
|
39
|
+
}">
|
|
40
|
+
${escapeHtml(item.label)}
|
|
41
|
+
</div>
|
|
42
|
+
<div class="mt-2 text-sm text-on-surface">${escapeHtml(item.value)}</div>
|
|
43
|
+
</div>
|
|
44
|
+
`
|
|
45
|
+
)
|
|
46
|
+
.join("")}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderPerformancePane(state) {
|
|
53
|
+
const metrics = getQueryPerformance(state);
|
|
54
|
+
|
|
55
|
+
return `
|
|
56
|
+
<div class="grid flex-1 grid-cols-1 gap-4 bg-surface-container-lowest p-6 md:grid-cols-4">
|
|
57
|
+
<div class="metric-card">
|
|
58
|
+
<span class="text-[10px] font-mono uppercase text-on-surface/40">Exec_Time</span>
|
|
59
|
+
<span class="font-headline text-3xl font-bold text-on-surface">${escapeHtml(
|
|
60
|
+
String(metrics.timingMs ?? 0)
|
|
61
|
+
)}ms</span>
|
|
62
|
+
<span class="text-[10px] text-primary-container">Measured backend execution time</span>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="metric-card">
|
|
65
|
+
<span class="text-[10px] font-mono uppercase text-on-surface/40">Statements</span>
|
|
66
|
+
<span class="font-headline text-3xl font-bold text-on-surface">${escapeHtml(
|
|
67
|
+
formatNumber(metrics.statementCount)
|
|
68
|
+
)}</span>
|
|
69
|
+
<span class="text-[10px] text-on-surface/40">Split and executed by SQLite</span>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="metric-card">
|
|
72
|
+
<span class="text-[10px] font-mono uppercase text-on-surface/40">Rows_Returned</span>
|
|
73
|
+
<span class="font-headline text-3xl font-bold text-on-surface">${escapeHtml(
|
|
74
|
+
formatNumber(metrics.rowCount)
|
|
75
|
+
)}</span>
|
|
76
|
+
<span class="text-[10px] text-on-surface/40">Visible result set size</span>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="metric-card metric-card--accent">
|
|
79
|
+
<span class="text-[10px] font-mono uppercase text-on-surface/40">Rows_Affected</span>
|
|
80
|
+
<span class="font-headline text-3xl font-bold text-on-surface">${escapeHtml(
|
|
81
|
+
formatNumber(metrics.affectedRowCount)
|
|
82
|
+
)}</span>
|
|
83
|
+
<span class="text-[10px] text-primary-container">INSERT / UPDATE / DELETE impact</span>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getResultEditingState(state) {
|
|
90
|
+
const editing = state.editor.result?.editing;
|
|
91
|
+
|
|
92
|
+
if (!editing) {
|
|
93
|
+
return {
|
|
94
|
+
enabled: false,
|
|
95
|
+
message: "",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (state.connections.active?.readOnly) {
|
|
100
|
+
return {
|
|
101
|
+
enabled: false,
|
|
102
|
+
message: "The active database is opened read-only, so query result editing is disabled.",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!editing.enabled) {
|
|
107
|
+
return {
|
|
108
|
+
enabled: false,
|
|
109
|
+
message: editing.reason || "Only direct single-table SELECT results can be edited here.",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
enabled: true,
|
|
115
|
+
message: `Click a row to edit it in ${editing.tableName}.`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getUniqueResultColumns(columns = []) {
|
|
120
|
+
const uniqueColumns = [];
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
|
|
123
|
+
columns.forEach((column) => {
|
|
124
|
+
if (!column?.sourceColumn || seen.has(column.sourceColumn)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
seen.add(column.sourceColumn);
|
|
129
|
+
uniqueColumns.push(column);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return uniqueColumns;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function renderEditorRowPanel(state) {
|
|
136
|
+
const result = state.editor.result;
|
|
137
|
+
const rowIndex = state.editor.selectedRowIndex;
|
|
138
|
+
const row = typeof rowIndex === "number" ? result?.rows?.[rowIndex] ?? null : null;
|
|
139
|
+
|
|
140
|
+
if (!result || !row || typeof rowIndex !== "number") {
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const uniqueColumns = getUniqueResultColumns(result.editing?.columns ?? []);
|
|
145
|
+
const editableColumns = uniqueColumns.filter((column) => {
|
|
146
|
+
if (column.identity || column.generated || !column.visible) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const value = row[column.resultName];
|
|
151
|
+
if (isBlobPreview(value) || (value && typeof value === "object")) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return true;
|
|
156
|
+
});
|
|
157
|
+
const readonlyColumns = uniqueColumns.filter((column) => {
|
|
158
|
+
if (!column.visible) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (column.identity || column.generated) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const value = row[column.resultName];
|
|
167
|
+
return isBlobPreview(value) || (value && typeof value === "object");
|
|
168
|
+
});
|
|
169
|
+
const editingState = getResultEditingState(state);
|
|
170
|
+
|
|
171
|
+
return renderRowEditorPanel({
|
|
172
|
+
title: result.editing?.tableName ?? "Query Result",
|
|
173
|
+
sectionLabel: "Row Editor",
|
|
174
|
+
subtitle: `query row ${rowIndex + 1}`,
|
|
175
|
+
closeAction: "clear-editor-row-selection",
|
|
176
|
+
formName: "save-editor-row",
|
|
177
|
+
hiddenFields: [{ name: "rowIndex", value: String(rowIndex) }],
|
|
178
|
+
disabledMessage: editingState.enabled ? "" : editingState.message,
|
|
179
|
+
editableFields: editableColumns.map((column) => {
|
|
180
|
+
const value = row[column.resultName];
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
name: column.sourceColumn,
|
|
184
|
+
label: column.sourceColumn,
|
|
185
|
+
value: value === null || value === undefined ? "" : String(value),
|
|
186
|
+
};
|
|
187
|
+
}),
|
|
188
|
+
readonlyFields: readonlyColumns.map((column) => ({
|
|
189
|
+
name: column.sourceColumn,
|
|
190
|
+
label: column.sourceColumn,
|
|
191
|
+
value: formatCellValue(row[column.resultName]),
|
|
192
|
+
})),
|
|
193
|
+
saveError: state.editor.saveError,
|
|
194
|
+
saving: state.editor.saving,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function renderResultsSurface(state, isResultsRoute) {
|
|
199
|
+
const activeTab = state.editor.activeTab;
|
|
200
|
+
const counts = {
|
|
201
|
+
resultRows: state.editor.result?.rows?.length ?? 0,
|
|
202
|
+
messages: getQueryMessages(state).length,
|
|
203
|
+
statementCount: state.editor.result?.statementCount ?? 0,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
let content = renderMessagesPane(state);
|
|
207
|
+
|
|
208
|
+
if (activeTab === "performance") {
|
|
209
|
+
content = renderPerformancePane(state);
|
|
210
|
+
} else if (activeTab === "results") {
|
|
211
|
+
const editingState = getResultEditingState(state);
|
|
212
|
+
|
|
213
|
+
content = state.connections.active
|
|
214
|
+
? renderQueryResultsPane(state.editor.result, {
|
|
215
|
+
exporting: state.editor.exportLoading,
|
|
216
|
+
selectedRowIndex: state.editor.selectedRowIndex,
|
|
217
|
+
editable: editingState.enabled,
|
|
218
|
+
editStatusMessage: editingState.message,
|
|
219
|
+
})
|
|
220
|
+
: renderMissingDatabase();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return `
|
|
224
|
+
<div class="flex h-full min-h-0 flex-col border-t border-outline-variant/10 bg-surface-container-lowest">
|
|
225
|
+
${renderBottomTabs(activeTab, counts)}
|
|
226
|
+
<div class="min-h-0 flex-1">${content}</div>
|
|
227
|
+
</div>
|
|
228
|
+
`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function renderEditorView(state, { isResultsRoute = false } = {}) {
|
|
232
|
+
const connection = getCurrentConnection(state);
|
|
233
|
+
const editorSectionClass = isResultsRoute ? "h-1/4" : "min-h-[27.5%]";
|
|
234
|
+
const resultsSectionClass = isResultsRoute ? "h-3/4" : "flex-1";
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
main: `
|
|
238
|
+
<section class="view-surface flex h-full min-h-0 flex-col overflow-hidden">
|
|
239
|
+
<div class="flex h-full min-h-0 flex-1 flex-col">
|
|
240
|
+
<section class="${editorSectionClass} flex min-h-0 flex-col ${
|
|
241
|
+
isResultsRoute ? "border-b-4 border-background" : ""
|
|
242
|
+
}">
|
|
243
|
+
${renderQueryEditor({
|
|
244
|
+
query: state.editor.sqlText,
|
|
245
|
+
executing: state.editor.executing,
|
|
246
|
+
history: state.editor.history,
|
|
247
|
+
historyLoading: state.editor.historyLoading,
|
|
248
|
+
title: connection?.label ?? "SQLite Query Workspace",
|
|
249
|
+
})}
|
|
250
|
+
</section>
|
|
251
|
+
<section class="${resultsSectionClass} flex min-h-0 flex-col overflow-hidden">
|
|
252
|
+
${renderResultsSurface(state, isResultsRoute)}
|
|
253
|
+
</section>
|
|
254
|
+
</div>
|
|
255
|
+
</section>
|
|
256
|
+
`,
|
|
257
|
+
panel: isResultsRoute ? renderEditorRowPanel(state) : "",
|
|
258
|
+
};
|
|
259
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { renderEmptyState } from "../components/emptyState.js";
|
|
2
|
+
|
|
3
|
+
export function renderLandingView(state) {
|
|
4
|
+
return {
|
|
5
|
+
main: renderEmptyState({
|
|
6
|
+
activeConnection: state.connections.active,
|
|
7
|
+
recentConnections: state.connections.recent,
|
|
8
|
+
}),
|
|
9
|
+
panel: "",
|
|
10
|
+
};
|
|
11
|
+
}
|