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