sqlite-hub 0.4.0 → 0.6.0
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/README.md +2 -2
- package/changelog.md +15 -0
- package/frontend/assets/mockups/connections.png +0 -0
- package/frontend/assets/mockups/data.png +0 -0
- package/frontend/assets/mockups/data_row_editor.png +0 -0
- package/frontend/assets/mockups/home.png +0 -0
- package/frontend/assets/mockups/sql_editor.png +0 -0
- package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
- package/frontend/assets/mockups/structure.png +0 -0
- package/frontend/assets/mockups/structure_inspector.png +0 -0
- package/frontend/js/api.js +114 -5
- package/frontend/js/app.js +368 -18
- package/frontend/js/components/bottomTabs.js +1 -1
- package/frontend/js/components/dataGrid.js +3 -3
- package/frontend/js/components/queryEditor.js +33 -55
- package/frontend/js/components/queryHistoryDetail.js +263 -0
- package/frontend/js/components/queryHistoryPanel.js +228 -0
- package/frontend/js/components/queryResults.js +32 -46
- package/frontend/js/components/rowEditorPanel.js +73 -14
- package/frontend/js/components/sidebar.js +1 -0
- package/frontend/js/components/tableDesignerEditor.js +356 -0
- package/frontend/js/components/tableDesignerSidebar.js +126 -0
- package/frontend/js/components/tableDesignerSqlPreview.js +40 -0
- package/frontend/js/router.js +10 -0
- package/frontend/js/store.js +841 -22
- package/frontend/js/utils/format.js +23 -0
- package/frontend/js/utils/tableDesigner.js +1192 -0
- package/frontend/js/views/data.js +273 -250
- package/frontend/js/views/editor.js +34 -10
- package/frontend/js/views/overview.js +15 -0
- package/frontend/js/views/tableDesigner.js +37 -0
- package/frontend/styles/base.css +87 -73
- package/frontend/styles/components.css +841 -188
- package/frontend/styles/views.css +40 -0
- package/package.json +1 -1
- package/server/routes/data.js +2 -0
- package/server/routes/export.js +4 -1
- package/server/routes/overview.js +12 -0
- package/server/routes/sql.js +163 -5
- package/server/routes/tableDesigner.js +60 -0
- package/server/server.js +5 -1
- package/server/services/sqlite/dataBrowserService.js +4 -16
- package/server/services/sqlite/exportService.js +4 -16
- package/server/services/sqlite/overviewService.js +34 -0
- package/server/services/sqlite/sqlExecutor.js +83 -63
- package/server/services/sqlite/tableDesigner/changeAnalysis.js +295 -0
- package/server/services/sqlite/tableDesigner/schemaMapping.js +233 -0
- package/server/services/sqlite/tableDesigner/sql.js +63 -0
- package/server/services/sqlite/tableDesigner/validation.js +245 -0
- package/server/services/sqlite/tableDesignerService.js +181 -0
- package/server/services/sqlite/tableSort.js +63 -0
- package/server/services/storage/appStateStore.js +674 -1
- package/server/services/storage/queryHistoryUtils.js +169 -0
- package/frontend/assets/mockups/data_edit.png +0 -0
- package/frontend/assets/mockups/graph_visualize.png +0 -0
- package/frontend/assets/mockups/overview.png +0 -0
|
@@ -1,29 +1,23 @@
|
|
|
1
|
-
import { renderDataGrid } from
|
|
2
|
-
import { renderRowEditorPanel } from
|
|
3
|
-
import {
|
|
4
|
-
escapeHtml,
|
|
5
|
-
formatCellValue,
|
|
6
|
-
formatNumber,
|
|
7
|
-
isBlobPreview,
|
|
8
|
-
truncateMiddle,
|
|
9
|
-
} from "../utils/format.js";
|
|
1
|
+
import { renderDataGrid } from '../components/dataGrid.js';
|
|
2
|
+
import { renderRowEditorPanel } from '../components/rowEditorPanel.js';
|
|
3
|
+
import { escapeHtml, formatCellValue, formatNumber, isBlobPreview, truncateMiddle } from '../utils/format.js';
|
|
10
4
|
|
|
11
5
|
function getSelectedRow(state) {
|
|
12
|
-
|
|
6
|
+
const rowIndex = state.dataBrowser.selectedRowIndex;
|
|
13
7
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
if (typeof rowIndex !== 'number') {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
17
11
|
|
|
18
|
-
|
|
12
|
+
return state.dataBrowser.table?.rows?.[rowIndex] ?? null;
|
|
19
13
|
}
|
|
20
14
|
|
|
21
15
|
function renderTableList(state) {
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
const tables = state.dataBrowser.tables ?? [];
|
|
17
|
+
const activeName = state.dataBrowser.selectedTable;
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
if (state.dataBrowser.loading && !tables.length) {
|
|
20
|
+
return `
|
|
27
21
|
<div class="flex flex-1 items-center justify-center px-6">
|
|
28
22
|
<div class="text-center text-on-surface-variant/40">
|
|
29
23
|
<span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
|
|
@@ -31,82 +25,82 @@ function renderTableList(state) {
|
|
|
31
25
|
</div>
|
|
32
26
|
</div>
|
|
33
27
|
`;
|
|
34
|
-
|
|
28
|
+
}
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
if (!tables.length) {
|
|
31
|
+
return `
|
|
38
32
|
<div class="px-6 py-6 text-sm text-on-surface-variant/55">
|
|
39
33
|
No tables found in the active SQLite database.
|
|
40
34
|
</div>
|
|
41
35
|
`;
|
|
42
|
-
|
|
36
|
+
}
|
|
43
37
|
|
|
44
|
-
|
|
38
|
+
return `
|
|
45
39
|
<div class="custom-scrollbar flex-1 overflow-auto px-4 py-4">
|
|
46
40
|
<div class="space-y-2">
|
|
47
41
|
${tables
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
.map(
|
|
43
|
+
table => `
|
|
50
44
|
<button
|
|
51
45
|
class="w-full border px-4 py-3 text-left transition-colors ${
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
table.name === activeName
|
|
47
|
+
? 'border-primary-container/30 bg-surface-container-high'
|
|
48
|
+
: 'border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high'
|
|
55
49
|
}"
|
|
56
50
|
data-action="navigate"
|
|
57
51
|
data-to="/data/${encodeURIComponent(table.name)}"
|
|
58
52
|
type="button"
|
|
59
53
|
>
|
|
60
54
|
<div class="truncate font-mono text-xs ${
|
|
61
|
-
|
|
55
|
+
table.name === activeName ? 'text-primary-container' : 'text-on-surface'
|
|
62
56
|
}">
|
|
63
57
|
${escapeHtml(table.name)}
|
|
64
58
|
</div>
|
|
65
59
|
</button>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
`,
|
|
61
|
+
)
|
|
62
|
+
.join('')}
|
|
69
63
|
</div>
|
|
70
64
|
</div>
|
|
71
65
|
`;
|
|
72
66
|
}
|
|
73
67
|
|
|
74
68
|
function renderWorkspaceHeader(state) {
|
|
75
|
-
|
|
69
|
+
const table = state.dataBrowser.table;
|
|
76
70
|
|
|
77
|
-
|
|
71
|
+
return `
|
|
78
72
|
<header class="border-b border-outline-variant/10 bg-surface-container px-6 py-5">
|
|
79
73
|
<div class="flex flex-wrap items-end justify-between gap-4">
|
|
80
|
-
<div>
|
|
74
|
+
<div class="data-headline-container">
|
|
81
75
|
<div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
|
|
82
76
|
Data Browser
|
|
83
77
|
</div>
|
|
84
78
|
<h1 class="mt-2 font-headline text-4xl font-black uppercase tracking-tight text-primary-container">
|
|
85
|
-
${escapeHtml(table?.name ??
|
|
79
|
+
${escapeHtml(table?.name ?? 'Table Data')}
|
|
86
80
|
</h1>
|
|
87
81
|
<div class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
88
82
|
${
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
83
|
+
table
|
|
84
|
+
? `rows ${escapeHtml(formatNumber(table.rowCount ?? 0))} // columns ${escapeHtml(
|
|
85
|
+
formatNumber(table.columns?.length ?? 0),
|
|
86
|
+
)}`
|
|
87
|
+
: `tables ${escapeHtml(formatNumber(state.dataBrowser.tables.length))}`
|
|
94
88
|
}
|
|
95
89
|
</div>
|
|
96
90
|
</div>
|
|
97
91
|
<div class="flex items-center gap-3">
|
|
98
92
|
${
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
table
|
|
94
|
+
? `
|
|
101
95
|
<button
|
|
102
96
|
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"
|
|
103
97
|
data-action="export-data-csv"
|
|
104
98
|
type="button"
|
|
105
99
|
>
|
|
106
|
-
${state.dataBrowser.exportLoading ?
|
|
100
|
+
${state.dataBrowser.exportLoading ? 'Exporting...' : 'Export CSV'}
|
|
107
101
|
</button>
|
|
108
102
|
`
|
|
109
|
-
|
|
103
|
+
: ''
|
|
110
104
|
}
|
|
111
105
|
<button
|
|
112
106
|
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"
|
|
@@ -116,8 +110,8 @@ function renderWorkspaceHeader(state) {
|
|
|
116
110
|
Reload Data
|
|
117
111
|
</button>
|
|
118
112
|
${
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
table
|
|
114
|
+
? `
|
|
121
115
|
<button
|
|
122
116
|
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"
|
|
123
117
|
data-action="navigate"
|
|
@@ -127,7 +121,7 @@ function renderWorkspaceHeader(state) {
|
|
|
127
121
|
Open Structure
|
|
128
122
|
</button>
|
|
129
123
|
`
|
|
130
|
-
|
|
124
|
+
: ''
|
|
131
125
|
}
|
|
132
126
|
</div>
|
|
133
127
|
</div>
|
|
@@ -136,11 +130,11 @@ function renderWorkspaceHeader(state) {
|
|
|
136
130
|
}
|
|
137
131
|
|
|
138
132
|
function renderWorkspaceError(state) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
133
|
+
if (!state.dataBrowser.error) {
|
|
134
|
+
return '';
|
|
135
|
+
}
|
|
142
136
|
|
|
143
|
-
|
|
137
|
+
return `
|
|
144
138
|
<div class="border-b border-error/20 bg-error-container/10 px-6 py-4 text-sm text-on-surface">
|
|
145
139
|
<div class="font-headline text-xs font-bold uppercase tracking-[0.18em] text-error">
|
|
146
140
|
${escapeHtml(state.dataBrowser.error.code)}
|
|
@@ -151,61 +145,93 @@ function renderWorkspaceError(state) {
|
|
|
151
145
|
}
|
|
152
146
|
|
|
153
147
|
function getCellWidthClass(columnName) {
|
|
154
|
-
|
|
148
|
+
const normalized = String(columnName ?? '').toLowerCase();
|
|
155
149
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
150
|
+
if (/(path|url|hash|sql|query|content|description|message|title|name)/.test(normalized)) {
|
|
151
|
+
return 'max-w-[18rem]';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (/(date|time|modified|created|updated|timestamp)/.test(normalized)) {
|
|
155
|
+
return 'max-w-[11rem]';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (/(id|uuid|token|key)/.test(normalized)) {
|
|
159
|
+
return 'max-w-[10rem]';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return 'max-w-[12rem]';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getSortIcon(columnName, sortColumn, sortDirection) {
|
|
166
|
+
if (columnName !== sortColumn) {
|
|
167
|
+
return 'unfold_more';
|
|
168
|
+
}
|
|
159
169
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
170
|
+
return sortDirection === 'desc' ? 'south' : 'north';
|
|
171
|
+
}
|
|
163
172
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
173
|
+
function renderSortableHeader(columnName, sortColumn, sortDirection, action) {
|
|
174
|
+
const isActive = columnName === sortColumn;
|
|
167
175
|
|
|
168
|
-
|
|
176
|
+
return `
|
|
177
|
+
<button
|
|
178
|
+
class="flex w-full items-center justify-between gap-2 text-left transition-colors ${
|
|
179
|
+
isActive ? 'text-primary-container' : 'text-on-surface-variant hover:text-primary-container'
|
|
180
|
+
}"
|
|
181
|
+
data-action="${action}"
|
|
182
|
+
data-column-name="${escapeHtml(columnName)}"
|
|
183
|
+
type="button"
|
|
184
|
+
>
|
|
185
|
+
<span class="truncate">${escapeHtml(columnName)}</span>
|
|
186
|
+
<span class="material-symbols-outlined text-sm leading-none">${getSortIcon(
|
|
187
|
+
columnName,
|
|
188
|
+
sortColumn,
|
|
189
|
+
sortDirection,
|
|
190
|
+
)}</span>
|
|
191
|
+
</button>
|
|
192
|
+
`;
|
|
169
193
|
}
|
|
170
194
|
|
|
171
195
|
function getFilteredTableRows(table, state) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
index
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
196
|
+
const allRows = table?.rows ?? [];
|
|
197
|
+
const availableColumns = table?.columns ?? [];
|
|
198
|
+
const searchQuery = String(state.dataBrowser.searchQuery ?? '')
|
|
199
|
+
.trim()
|
|
200
|
+
.toLowerCase();
|
|
201
|
+
const activeColumn = availableColumns.includes(state.dataBrowser.searchColumn)
|
|
202
|
+
? state.dataBrowser.searchColumn
|
|
203
|
+
: (availableColumns[0] ?? '');
|
|
204
|
+
|
|
205
|
+
const indexedRows = allRows.map((row, index) => ({
|
|
206
|
+
row,
|
|
207
|
+
index,
|
|
208
|
+
}));
|
|
209
|
+
|
|
210
|
+
if (!searchQuery || !activeColumn) {
|
|
211
|
+
return {
|
|
212
|
+
activeColumn,
|
|
213
|
+
filteredRows: indexedRows,
|
|
214
|
+
searchQuery,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
185
218
|
return {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
219
|
+
activeColumn,
|
|
220
|
+
searchQuery,
|
|
221
|
+
filteredRows: indexedRows.filter(({ row }) =>
|
|
222
|
+
formatCellValue(row[activeColumn]).toLowerCase().includes(searchQuery),
|
|
223
|
+
),
|
|
189
224
|
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
activeColumn,
|
|
194
|
-
searchQuery,
|
|
195
|
-
filteredRows: indexedRows.filter(({ row }) =>
|
|
196
|
-
formatCellValue(row[activeColumn]).toLowerCase().includes(searchQuery)
|
|
197
|
-
),
|
|
198
|
-
};
|
|
199
225
|
}
|
|
200
226
|
|
|
201
227
|
function renderTableSearchBar(table, state, activeColumn, filteredRowCount) {
|
|
202
|
-
|
|
228
|
+
const columns = table?.columns ?? [];
|
|
203
229
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
230
|
+
if (!table || !columns.length) {
|
|
231
|
+
return '';
|
|
232
|
+
}
|
|
207
233
|
|
|
208
|
-
|
|
234
|
+
return `
|
|
209
235
|
<div class="flex flex-wrap items-center gap-3 border-b border-outline-variant/10 bg-surface-container-low px-6 py-4">
|
|
210
236
|
<label class="flex min-w-[18rem] flex-1 items-center gap-3 border border-outline-variant/20 bg-surface-container-lowest px-4 py-3">
|
|
211
237
|
<span class="material-symbols-outlined text-base text-on-surface-variant/55">search</span>
|
|
@@ -214,7 +240,7 @@ function renderTableSearchBar(table, state, activeColumn, filteredRowCount) {
|
|
|
214
240
|
data-bind="data-search-query"
|
|
215
241
|
placeholder="Filter current page..."
|
|
216
242
|
type="search"
|
|
217
|
-
value="${escapeHtml(state.dataBrowser.searchQuery ??
|
|
243
|
+
value="${escapeHtml(state.dataBrowser.searchQuery ?? '')}"
|
|
218
244
|
/>
|
|
219
245
|
</label>
|
|
220
246
|
<select
|
|
@@ -222,31 +248,27 @@ function renderTableSearchBar(table, state, activeColumn, filteredRowCount) {
|
|
|
222
248
|
data-bind="data-search-column"
|
|
223
249
|
>
|
|
224
250
|
${columns
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
<option value="${escapeHtml(columnName)}" ${
|
|
228
|
-
columnName === activeColumn ? "selected" : ""
|
|
229
|
-
}>
|
|
251
|
+
.map(
|
|
252
|
+
columnName => `
|
|
253
|
+
<option value="${escapeHtml(columnName)}" ${columnName === activeColumn ? 'selected' : ''}>
|
|
230
254
|
${escapeHtml(columnName)}
|
|
231
255
|
</option>
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
256
|
+
`,
|
|
257
|
+
)
|
|
258
|
+
.join('')}
|
|
235
259
|
</select>
|
|
236
260
|
<div class="text-[10px] font-mono tracking-[0.14em] text-on-surface-variant/55">
|
|
237
|
-
${escapeHtml(formatNumber(filteredRowCount))} match${
|
|
238
|
-
filteredRowCount === 1 ? "" : "es"
|
|
239
|
-
} on this page
|
|
261
|
+
${escapeHtml(formatNumber(filteredRowCount))} match${filteredRowCount === 1 ? '' : 'es'} on this page
|
|
240
262
|
</div>
|
|
241
263
|
</div>
|
|
242
264
|
`;
|
|
243
265
|
}
|
|
244
266
|
|
|
245
267
|
function renderTableSurface(state) {
|
|
246
|
-
|
|
268
|
+
const table = state.dataBrowser.table;
|
|
247
269
|
|
|
248
|
-
|
|
249
|
-
|
|
270
|
+
if (state.dataBrowser.tableLoading && !table) {
|
|
271
|
+
return `
|
|
250
272
|
<div class="flex flex-1 items-center justify-center">
|
|
251
273
|
<div class="text-center text-on-surface-variant/40">
|
|
252
274
|
<span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
|
|
@@ -254,10 +276,10 @@ function renderTableSurface(state) {
|
|
|
254
276
|
</div>
|
|
255
277
|
</div>
|
|
256
278
|
`;
|
|
257
|
-
|
|
279
|
+
}
|
|
258
280
|
|
|
259
|
-
|
|
260
|
-
|
|
281
|
+
if (!table) {
|
|
282
|
+
return `
|
|
261
283
|
<div class="flex flex-1 items-center justify-center px-8 text-center">
|
|
262
284
|
<div>
|
|
263
285
|
<div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
|
|
@@ -269,86 +291,88 @@ function renderTableSurface(state) {
|
|
|
269
291
|
</div>
|
|
270
292
|
</div>
|
|
271
293
|
`;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const { activeColumn, filteredRows, searchQuery } = getFilteredTableRows(table, state);
|
|
297
|
+
const sortColumn = state.dataBrowser.sortColumn;
|
|
298
|
+
const sortDirection = state.dataBrowser.sortDirection;
|
|
299
|
+
const columns = (table.columns ?? []).map(columnName => ({
|
|
300
|
+
headerClassName:
|
|
301
|
+
'border-b border-primary-container/20 px-4 py-3 text-[10px] font-bold tracking-[0.08em] text-primary-container',
|
|
302
|
+
renderHeader: () => renderSortableHeader(columnName, sortColumn, sortDirection, 'sort-data-column'),
|
|
303
|
+
cellClassName: 'px-4 py-2 align-top text-[11px] text-on-surface',
|
|
304
|
+
render: row => {
|
|
305
|
+
const value = formatCellValue(row[columnName]);
|
|
306
|
+
const isNull = value === 'NULL';
|
|
307
|
+
const widthClass = getCellWidthClass(columnName);
|
|
308
|
+
const displayValue = isNull ? value : truncateMiddle(value, 48);
|
|
309
|
+
|
|
310
|
+
return `<span class="block ${widthClass} overflow-hidden text-ellipsis whitespace-nowrap ${
|
|
311
|
+
isNull ? 'text-on-surface-variant/45' : 'text-on-surface'
|
|
312
|
+
}" title="${escapeHtml(value)}">${escapeHtml(displayValue)}</span>`;
|
|
313
|
+
},
|
|
314
|
+
}));
|
|
315
|
+
const totalRows = table.rowCount ?? 0;
|
|
316
|
+
const page = table.page ?? state.dataBrowser.page ?? 1;
|
|
317
|
+
const pageCount = table.pageCount ?? Math.max(1, Math.ceil(totalRows / (table.limit ?? 50)));
|
|
318
|
+
const fromRow = totalRows === 0 ? 0 : (table.offset ?? 0) + 1;
|
|
319
|
+
const toRow = totalRows === 0 ? 0 : Math.min((table.offset ?? 0) + (table.rows?.length ?? 0), totalRows);
|
|
320
|
+
const pageSizes = [25, 50, 100];
|
|
321
|
+
const filteredRowCount = filteredRows.length;
|
|
322
|
+
const hasActiveSearch = Boolean(searchQuery);
|
|
323
|
+
|
|
324
|
+
return `
|
|
301
325
|
<div class="flex flex-1 min-h-0 flex-col bg-surface-container-lowest">
|
|
302
326
|
${renderTableSearchBar(table, state, activeColumn, filteredRowCount)}
|
|
303
327
|
<div class="custom-scrollbar flex-1 overflow-auto">
|
|
304
328
|
${renderDataGrid({
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
329
|
+
columns,
|
|
330
|
+
rows: filteredRows.map(({ row }) => row),
|
|
331
|
+
tableClass: 'min-w-full border-collapse text-left font-mono text-xs',
|
|
332
|
+
theadClass: 'sticky top-0 z-10 bg-surface-container-highest',
|
|
333
|
+
tbodyClass: 'divide-y divide-outline-variant/5',
|
|
334
|
+
getRowClass: (_, filteredIndex) => {
|
|
335
|
+
const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
|
|
336
|
+
|
|
337
|
+
return `${
|
|
338
|
+
state.dataBrowser.selectedRowIndex === rowIndex
|
|
339
|
+
? 'bg-surface-bright'
|
|
340
|
+
: filteredIndex % 2 === 0
|
|
341
|
+
? 'bg-surface-container-low'
|
|
342
|
+
: 'bg-surface-container-lowest'
|
|
343
|
+
} cursor-pointer transition-colors hover:bg-surface-container-high`;
|
|
344
|
+
},
|
|
345
|
+
getRowAttrs: (_, filteredIndex) => {
|
|
346
|
+
const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
|
|
347
|
+
|
|
348
|
+
return `data-action="select-data-row" data-row-index="${rowIndex}"`;
|
|
349
|
+
},
|
|
326
350
|
})}
|
|
327
351
|
${
|
|
328
|
-
|
|
329
|
-
|
|
352
|
+
!table.rows?.length
|
|
353
|
+
? `
|
|
330
354
|
<div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10">
|
|
331
355
|
<p class="font-mono text-[10px] uppercase tracking-[0.22em] text-on-surface-variant/40">
|
|
332
356
|
TABLE_IS_EMPTY
|
|
333
357
|
</p>
|
|
334
358
|
</div>
|
|
335
359
|
`
|
|
336
|
-
|
|
337
|
-
|
|
360
|
+
: !filteredRowCount
|
|
361
|
+
? `
|
|
338
362
|
<div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10">
|
|
339
363
|
<p class="font-mono text-[10px] tracking-[0.18em] text-on-surface-variant/40">
|
|
340
|
-
${hasActiveSearch ?
|
|
364
|
+
${hasActiveSearch ? 'No matching rows on this page.' : 'No rows available.'}
|
|
341
365
|
</p>
|
|
342
366
|
</div>
|
|
343
367
|
`
|
|
344
|
-
|
|
368
|
+
: ''
|
|
345
369
|
}
|
|
346
370
|
</div>
|
|
347
371
|
<footer class="flex flex-wrap items-center justify-between gap-4 border-t border-outline-variant/10 bg-surface-container px-6 py-4">
|
|
348
372
|
<div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
349
373
|
showing ${escapeHtml(formatNumber(fromRow))}-${escapeHtml(formatNumber(toRow))} of ${escapeHtml(
|
|
350
|
-
|
|
351
|
-
)} rows${hasActiveSearch ? ` // ${escapeHtml(formatNumber(filteredRowCount))} visible on this page` :
|
|
374
|
+
formatNumber(totalRows),
|
|
375
|
+
)} rows${hasActiveSearch ? ` // ${escapeHtml(formatNumber(filteredRowCount))} visible on this page` : ''}
|
|
352
376
|
</div>
|
|
353
377
|
<div class="flex flex-wrap items-center gap-4">
|
|
354
378
|
<div class="flex items-center gap-2">
|
|
@@ -357,13 +381,13 @@ function renderTableSurface(state) {
|
|
|
357
381
|
</span>
|
|
358
382
|
<div class="flex items-center gap-2">
|
|
359
383
|
${pageSizes
|
|
360
|
-
|
|
361
|
-
|
|
384
|
+
.map(
|
|
385
|
+
pageSize => `
|
|
362
386
|
<button
|
|
363
387
|
class="border px-3 py-2 text-[10px] font-bold uppercase tracking-[0.16em] transition-colors ${
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
388
|
+
pageSize === (table.limit ?? state.dataBrowser.pageSize)
|
|
389
|
+
? 'border-primary-container/30 bg-surface-container-high text-primary-container'
|
|
390
|
+
: 'border-outline-variant/20 text-on-surface hover:bg-surface-container-highest'
|
|
367
391
|
}"
|
|
368
392
|
data-action="set-data-page-size"
|
|
369
393
|
data-page-size="${pageSize}"
|
|
@@ -371,9 +395,9 @@ function renderTableSurface(state) {
|
|
|
371
395
|
>
|
|
372
396
|
${pageSize}
|
|
373
397
|
</button>
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
398
|
+
`,
|
|
399
|
+
)
|
|
400
|
+
.join('')}
|
|
377
401
|
</div>
|
|
378
402
|
</div>
|
|
379
403
|
<div class="flex items-center gap-2">
|
|
@@ -382,7 +406,7 @@ function renderTableSurface(state) {
|
|
|
382
406
|
data-action="set-data-page"
|
|
383
407
|
data-page="${page - 1}"
|
|
384
408
|
type="button"
|
|
385
|
-
${page <= 1 ?
|
|
409
|
+
${page <= 1 ? 'disabled' : ''}
|
|
386
410
|
>
|
|
387
411
|
Prev
|
|
388
412
|
</button>
|
|
@@ -394,7 +418,7 @@ function renderTableSurface(state) {
|
|
|
394
418
|
data-action="set-data-page"
|
|
395
419
|
data-page="${page + 1}"
|
|
396
420
|
type="button"
|
|
397
|
-
${page >= pageCount ?
|
|
421
|
+
${page >= pageCount ? 'disabled' : ''}
|
|
398
422
|
>
|
|
399
423
|
Next
|
|
400
424
|
</button>
|
|
@@ -406,84 +430,83 @@ function renderTableSurface(state) {
|
|
|
406
430
|
}
|
|
407
431
|
|
|
408
432
|
function renderDataRowEditorPanel(state) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if (!table || !row || typeof rowIndex !== "number") {
|
|
414
|
-
return "";
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const identityColumns =
|
|
418
|
-
table.identityStrategy?.type === "primaryKey" ? table.identityStrategy.columns ?? [] : [];
|
|
419
|
-
const editableColumns = (table.columnMeta ?? []).filter((column) => {
|
|
420
|
-
if (!column.visible || column.generated) {
|
|
421
|
-
return false;
|
|
422
|
-
}
|
|
433
|
+
const table = state.dataBrowser.table;
|
|
434
|
+
const rowIndex = state.dataBrowser.selectedRowIndex;
|
|
435
|
+
const row = getSelectedRow(state);
|
|
423
436
|
|
|
424
|
-
if (
|
|
425
|
-
|
|
437
|
+
if (!table || !row || typeof rowIndex !== 'number') {
|
|
438
|
+
return '';
|
|
426
439
|
}
|
|
427
440
|
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
441
|
+
const identityColumns = table.identityStrategy?.type === 'primaryKey' ? (table.identityStrategy.columns ?? []) : [];
|
|
442
|
+
const editableColumns = (table.columnMeta ?? []).filter(column => {
|
|
443
|
+
if (!column.visible || column.generated) {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
432
446
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (!column.visible) {
|
|
437
|
-
return false;
|
|
438
|
-
}
|
|
447
|
+
if (identityColumns.includes(column.name)) {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
439
450
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
451
|
+
const value = row[column.name];
|
|
452
|
+
if (isBlobPreview(value) || (value && typeof value === 'object')) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
443
455
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
456
|
+
return true;
|
|
457
|
+
});
|
|
458
|
+
const readonlyColumns = (table.columnMeta ?? []).filter(column => {
|
|
459
|
+
if (!column.visible) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (identityColumns.includes(column.name) || column.generated) {
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const value = row[column.name];
|
|
468
|
+
return isBlobPreview(value) || (value && typeof value === 'object');
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
return renderRowEditorPanel({
|
|
472
|
+
title: table.name,
|
|
473
|
+
sectionLabel: 'Row Editor',
|
|
474
|
+
subtitle: `row ${rowIndex + 1}`,
|
|
475
|
+
closeAction: 'clear-data-row-selection',
|
|
476
|
+
formName: 'save-data-row',
|
|
477
|
+
hiddenFields: [{ name: 'rowIndex', value: String(rowIndex) }],
|
|
478
|
+
disabledMessage: state.connections.active?.readOnly
|
|
479
|
+
? 'The active database is opened read-only, so row editing is disabled.'
|
|
480
|
+
: table.notSafelyUpdatable
|
|
481
|
+
? 'This table has no stable identity column, so SQLite Hub cannot safely update rows.'
|
|
482
|
+
: '',
|
|
483
|
+
editableFields: editableColumns.map(column => {
|
|
484
|
+
const value = row[column.name];
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
name: column.name,
|
|
488
|
+
label: column.name,
|
|
489
|
+
value: value === null || value === undefined ? '' : String(value),
|
|
490
|
+
};
|
|
491
|
+
}),
|
|
492
|
+
readonlyFields: readonlyColumns.map(column => ({
|
|
493
|
+
name: column.name,
|
|
494
|
+
label: column.name,
|
|
495
|
+
value: formatCellValue(row[column.name]),
|
|
496
|
+
})),
|
|
497
|
+
saveError: state.dataBrowser.saveError,
|
|
498
|
+
saving: state.dataBrowser.saving,
|
|
499
|
+
deleting: state.dataBrowser.deleting,
|
|
500
|
+
deleteAction: 'delete-data-row',
|
|
501
|
+
deleteRowIndex: rowIndex,
|
|
502
|
+
deleteEnabled: Boolean(row.__identity),
|
|
503
|
+
reloadAction: 'reload-data-route',
|
|
504
|
+
});
|
|
482
505
|
}
|
|
483
506
|
|
|
484
507
|
export function renderDataView(state) {
|
|
485
|
-
|
|
486
|
-
|
|
508
|
+
return {
|
|
509
|
+
main: `
|
|
487
510
|
<section class="view-surface min-h-full bg-surface-container flex h-full min-h-0 flex-col overflow-hidden">
|
|
488
511
|
<div class="grid h-full min-h-0 grid-cols-1 md:grid-cols-[18rem_minmax(0,1fr)]">
|
|
489
512
|
<aside class="flex min-h-0 flex-col border-r border-outline-variant/10 bg-surface-low">
|
|
@@ -505,6 +528,6 @@ export function renderDataView(state) {
|
|
|
505
528
|
</div>
|
|
506
529
|
</section>
|
|
507
530
|
`,
|
|
508
|
-
|
|
509
|
-
|
|
531
|
+
panel: renderDataRowEditorPanel(state),
|
|
532
|
+
};
|
|
510
533
|
}
|