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.
Files changed (50) hide show
  1. package/README.md +109 -2
  2. package/bin/sqlite-hub.js +285 -4
  3. package/changelog.md +15 -0
  4. package/frontend/assets/mockups/connections.png +0 -0
  5. package/frontend/assets/mockups/data.png +0 -0
  6. package/frontend/assets/mockups/data_row_editor.png +0 -0
  7. package/frontend/assets/mockups/home.png +0 -0
  8. package/frontend/assets/mockups/sql_editor.png +0 -0
  9. package/frontend/assets/mockups/sql_editor_croped.png +0 -0
  10. package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
  11. package/frontend/assets/mockups/structure.png +0 -0
  12. package/frontend/assets/mockups/structure_inspector.png +0 -0
  13. package/frontend/index.html +1 -0
  14. package/frontend/js/api.js +49 -0
  15. package/frontend/js/app.js +377 -9
  16. package/frontend/js/components/modal.js +314 -1
  17. package/frontend/js/components/queryChartRenderer.js +125 -0
  18. package/frontend/js/components/queryEditor.js +37 -8
  19. package/frontend/js/components/queryHistoryPanel.js +16 -5
  20. package/frontend/js/components/sidebar.js +2 -0
  21. package/frontend/js/components/tableDesignerEditor.js +356 -0
  22. package/frontend/js/components/tableDesignerSidebar.js +129 -0
  23. package/frontend/js/components/tableDesignerSqlPreview.js +40 -0
  24. package/frontend/js/lib/queryChartOptions.js +283 -0
  25. package/frontend/js/lib/queryCharts.js +560 -0
  26. package/frontend/js/router.js +18 -0
  27. package/frontend/js/store.js +914 -1
  28. package/frontend/js/utils/tableDesigner.js +1192 -0
  29. package/frontend/js/views/charts.js +471 -0
  30. package/frontend/js/views/data.js +281 -277
  31. package/frontend/js/views/editor.js +19 -13
  32. package/frontend/js/views/structure.js +81 -39
  33. package/frontend/js/views/tableDesigner.js +37 -0
  34. package/frontend/styles/base.css +87 -73
  35. package/frontend/styles/components.css +790 -251
  36. package/frontend/styles/views.css +316 -46
  37. package/package.json +2 -1
  38. package/server/routes/charts.js +153 -0
  39. package/server/routes/tableDesigner.js +60 -0
  40. package/server/server.js +10 -0
  41. package/server/services/sqlite/tableDesigner/changeAnalysis.js +295 -0
  42. package/server/services/sqlite/tableDesigner/schemaMapping.js +233 -0
  43. package/server/services/sqlite/tableDesigner/sql.js +63 -0
  44. package/server/services/sqlite/tableDesigner/validation.js +245 -0
  45. package/server/services/sqlite/tableDesignerService.js +181 -0
  46. package/server/services/storage/appStateStore.js +400 -1
  47. package/server/services/storage/queryHistoryChartUtils.js +145 -0
  48. package/frontend/assets/mockups/data_edit.png +0 -0
  49. package/frontend/assets/mockups/graph_visualize.png +0 -0
  50. package/frontend/assets/mockups/overview.png +0 -0
@@ -0,0 +1,471 @@
1
+ import { renderQueryResultsPane } from '../components/queryResults.js';
2
+ import { analyzeQueryChartResult, getQueryChartTypeLabel, validateQueryChartConfig } from '../lib/queryCharts.js';
3
+ import { escapeHtml, highlightSql } from '../utils/format.js';
4
+ import { renderStatusBadge } from '../components/badges.js';
5
+
6
+ function renderMissingDatabase() {
7
+ return `
8
+ <section class="flex flex-1 items-center justify-center px-8 text-center">
9
+ <div class="max-w-xl">
10
+ <span class="material-symbols-outlined mb-4 text-5xl text-on-surface-variant/20">database_off</span>
11
+ <h1 class="font-headline text-3xl font-black uppercase tracking-tight text-primary-container">
12
+ No Active SQLite Database
13
+ </h1>
14
+ <p class="mt-3 text-sm leading-7 text-on-surface-variant/65">
15
+ Open a local SQLite database first. The Charts area only works against query-history entries from the active database.
16
+ </p>
17
+ </div>
18
+ </section>
19
+ `;
20
+ }
21
+
22
+ function renderChartsList(state) {
23
+ const queries = state.charts.queries ?? [];
24
+ const selectedHistoryId = Number(state.charts.selectedHistoryId);
25
+
26
+ if (state.charts.loading && !queries.length) {
27
+ return `
28
+ <div class="flex h-full items-center justify-center px-6 text-center text-on-surface-variant/45">
29
+ <div>
30
+ <span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
31
+ <p class="font-mono text-[10px] uppercase tracking-[0.18em]">Loading Queries</p>
32
+ </div>
33
+ </div>
34
+ `;
35
+ }
36
+
37
+ if (state.charts.error && !queries.length) {
38
+ return `
39
+ <div class="p-5">
40
+ <div class="border border-error/30 bg-error-container/20 px-4 py-4 text-sm text-error">
41
+ ${escapeHtml(state.charts.error.message)}
42
+ </div>
43
+ </div>
44
+ `;
45
+ }
46
+
47
+ if (!queries.length) {
48
+ return `
49
+ <div class="flex h-full items-center justify-center px-6 text-center">
50
+ <div>
51
+ <span class="material-symbols-outlined mb-3 text-4xl text-on-surface-variant/25">query_stats</span>
52
+ <p class="font-headline text-lg font-black uppercase tracking-tight text-on-surface">
53
+ No Query History
54
+ </p>
55
+ <p class="mt-2 max-w-xs text-sm leading-6 text-on-surface-variant/60">
56
+ Run saved queries in the SQL Editor first. They will appear here automatically.
57
+ </p>
58
+ </div>
59
+ </div>
60
+ `;
61
+ }
62
+
63
+ return `
64
+ <div class="custom-scrollbar flex-1 overflow-auto px-4 py-4">
65
+ <div class="space-y-2">
66
+ ${queries
67
+ .map(
68
+ item => `
69
+ <button
70
+ class="w-full border px-4 py-3 text-left transition-colors ${
71
+ selectedHistoryId === item.id
72
+ ? 'border-primary-container/30 bg-surface-container-high'
73
+ : 'border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high'
74
+ }"
75
+ data-action="navigate"
76
+ data-to="/charts/${encodeURIComponent(item.id)}"
77
+ type="button"
78
+ >
79
+ <div class="flex items-start justify-between gap-3">
80
+ <div class="min-w-0 flex-1 truncate font-mono text-xs ${
81
+ selectedHistoryId === item.id ? 'text-primary-container' : 'text-on-surface'
82
+ }">
83
+ ${escapeHtml(item.displayTitle)}
84
+ </div>
85
+ <div class="flex shrink-0 flex-wrap justify-end gap-1">
86
+ ${
87
+ item.chartTypes?.length
88
+ ? item.chartTypes
89
+ .map(chartType => renderStatusBadge(getQueryChartTypeLabel(chartType), 'primary'))
90
+ .join('')
91
+ : renderStatusBadge('None', 'muted')
92
+ }
93
+ </div>
94
+ </div>
95
+ <div class="mt-1 truncate text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">
96
+ ${escapeHtml(item.previewSql)}
97
+ </div>
98
+ </button>
99
+ `,
100
+ )
101
+ .join('')}
102
+ </div>
103
+ </div>
104
+ `;
105
+ }
106
+
107
+ function renderEmptyChartDetail() {
108
+ return `
109
+ <div class="flex flex-1 items-center justify-center px-8 py-10 text-center">
110
+ <div class="max-w-lg">
111
+ <span class="material-symbols-outlined mb-4 text-5xl text-on-surface-variant/20">bar_chart</span>
112
+ <h2 class="font-headline text-2xl font-black uppercase tracking-tight text-primary-container">
113
+ Select A Query
114
+ </h2>
115
+ <p class="mt-3 text-sm leading-7 text-on-surface-variant/65">
116
+ Choose a query-history entry on the left to load its charts, render the live result set, and manage chart definitions.
117
+ </p>
118
+ </div>
119
+ </div>
120
+ `;
121
+ }
122
+
123
+ function renderQueryResultState(state, result) {
124
+ if (state.charts.resultLoading && !result) {
125
+ return `
126
+ <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-4 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
127
+ Loading live query result...
128
+ </div>
129
+ `;
130
+ }
131
+
132
+ if (state.charts.resultError) {
133
+ return `
134
+ <div class="border border-error/30 bg-error-container/15 px-4 py-4 text-sm text-error">
135
+ <div class="font-mono text-[10px] uppercase tracking-[0.18em]">
136
+ ${escapeHtml(state.charts.resultError.code ?? 'RESULT_ERROR')}
137
+ </div>
138
+ <div class="mt-2">${escapeHtml(state.charts.resultError.message)}</div>
139
+ </div>
140
+ `;
141
+ }
142
+
143
+ if (!result) {
144
+ return `
145
+ <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-4 text-sm text-on-surface-variant/60">
146
+ No live result set is available for this query.
147
+ </div>
148
+ `;
149
+ }
150
+
151
+ return '';
152
+ }
153
+
154
+ function renderQuerySqlSection(state, rawSql) {
155
+ const isExpanded = Boolean(state.charts.sqlExpanded);
156
+
157
+ return `
158
+ <section class="mt-5 border border-outline-variant/10 bg-surface-container-lowest">
159
+ <button
160
+ class="flex w-full items-center justify-between gap-3 border-b border-outline-variant/10 px-4 py-3 text-left"
161
+ data-action="toggle-charts-sql-panel"
162
+ type="button"
163
+ >
164
+ <span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
165
+ Query SQL
166
+ </span>
167
+ <span class="material-symbols-outlined text-on-surface-variant/55">
168
+ ${isExpanded ? 'expand_less' : 'expand_more'}
169
+ </span>
170
+ </button>
171
+ ${
172
+ isExpanded
173
+ ? `
174
+ <pre class="custom-scrollbar overflow-auto p-4 font-mono text-sm leading-6 text-on-surface"><code>${highlightSql(
175
+ rawSql,
176
+ )}</code></pre>
177
+ `
178
+ : ''
179
+ }
180
+ </section>
181
+ `;
182
+ }
183
+
184
+ function renderQueryResultsSection(state) {
185
+ const isVisible = Boolean(state.charts.resultsVisible);
186
+ const statusMarkup = renderQueryResultState(state, state.charts.result);
187
+
188
+ return `
189
+ <section class="mt-6 border border-outline-variant/10 bg-surface-container-lowest">
190
+ <div class="flex items-center justify-between gap-3 border-b border-outline-variant/10 px-4 py-3">
191
+ <span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
192
+ Results
193
+ </span>
194
+ <button
195
+ class="toolbar-button query-chart-card__action"
196
+ data-action="toggle-charts-results-panel"
197
+ type="button"
198
+ >
199
+ ${isVisible ? 'Hide Results' : 'Show Results'}
200
+ </button>
201
+ </div>
202
+ ${statusMarkup}
203
+ ${
204
+ isVisible && state.charts.result && !state.charts.resultLoading && !state.charts.resultError
205
+ ? `
206
+ <div class="h-[18rem] overflow-hidden">
207
+ ${renderQueryResultsPane(state.charts.result, {
208
+ selectedRowIndex: null,
209
+ editable: false,
210
+ sortColumn: null,
211
+ sortDirection: null,
212
+ })}
213
+ </div>
214
+ `
215
+ : ''
216
+ }
217
+ </section>
218
+ `;
219
+ }
220
+
221
+ function resolveChartCardSizeClass(state) {
222
+ return `query-chart-card--${String(state.charts.chartHeightPreset ?? 'medium')
223
+ .trim()
224
+ .toLowerCase()}`;
225
+ }
226
+
227
+ function renderChartHeightPresetToggle(state) {
228
+ const activePreset = String(state.charts.chartHeightPreset ?? 'medium')
229
+ .trim()
230
+ .toLowerCase();
231
+ const presets = [
232
+ { value: 'small', label: 'Small', height: '300px' },
233
+ { value: 'medium', label: 'Medium', height: '450px' },
234
+ { value: 'large', label: 'Large', height: '600px' },
235
+ ];
236
+
237
+ return `
238
+ <div class="flex flex-col gap-2">
239
+ <div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
240
+ Chart Height
241
+ </div>
242
+ <div class="charts-height-toggle" role="group" aria-label="Chart height preset">
243
+ ${presets
244
+ .map(
245
+ preset => `
246
+ <button
247
+ class="toolbar-button charts-height-toggle__button ${activePreset === preset.value ? 'is-active' : ''}"
248
+ data-action="set-charts-height-preset"
249
+ data-preset="${preset.value}"
250
+ type="button"
251
+ title="${preset.height}"
252
+ >
253
+ ${preset.label}
254
+ </button>
255
+ `,
256
+ )
257
+ .join('')}
258
+ </div>
259
+ </div>
260
+ `;
261
+ }
262
+
263
+ function renderChartSurface(chart, state, analysis) {
264
+ if (state.charts.resultLoading) {
265
+ return `
266
+ <div class="query-chart-surface-state flex h-full items-center justify-center text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
267
+ Loading chart result...
268
+ </div>
269
+ `;
270
+ }
271
+
272
+ if (state.charts.resultError) {
273
+ return `
274
+ <div class="query-chart-surface-state flex items-center justify-center border border-error/20 bg-error-container/10 px-6 text-center text-sm text-error">
275
+ ${escapeHtml(state.charts.resultError.message)}
276
+ </div>
277
+ `;
278
+ }
279
+
280
+ const validation = validateQueryChartConfig(chart.chartType, chart.config, analysis);
281
+
282
+ if (!validation.valid) {
283
+ return `
284
+ <div class="query-chart-surface-state flex items-center justify-center border border-error/20 bg-error-container/10 px-6 text-center text-sm text-error">
285
+ ${escapeHtml(validation.errors.join(' '))}
286
+ </div>
287
+ `;
288
+ }
289
+
290
+ return `
291
+ <div
292
+ class="query-chart-canvas w-full"
293
+ data-query-chart-id="${escapeHtml(chart.id)}"
294
+ data-chart-export-name="${escapeHtml(chart.name.replace(/[^\w.-]+/g, '_'))}"
295
+ ></div>
296
+ `;
297
+ }
298
+
299
+ function renderChartCard(chart, state, analysis) {
300
+ const sizeClass = resolveChartCardSizeClass(state);
301
+
302
+ return `
303
+ <article class="query-chart-card ${sizeClass}">
304
+ <header class="query-chart-card__header">
305
+ <div class="min-w-0">
306
+ <div class="flex flex-wrap items-center gap-2">
307
+ <h3 class="truncate font-headline text-xl font-black uppercase tracking-tight text-on-surface">
308
+ ${escapeHtml(chart.name)}
309
+ </h3>
310
+ ${renderStatusBadge(getQueryChartTypeLabel(chart.chartType), 'primary')}
311
+ </div>
312
+ </div>
313
+ <div class="flex flex-wrap items-center gap-2">
314
+ <button
315
+ class="toolbar-button query-chart-card__action"
316
+ data-action="open-edit-query-chart-modal"
317
+ data-chart-id="${escapeHtml(chart.id)}"
318
+ type="button"
319
+ >
320
+ Edit
321
+ </button>
322
+ <button
323
+ class="toolbar-button query-chart-card__action"
324
+ data-action="open-delete-query-chart-modal"
325
+ data-chart-id="${escapeHtml(chart.id)}"
326
+ type="button"
327
+ >
328
+ Delete
329
+ </button>
330
+ <button
331
+ class="toolbar-button query-chart-card__action"
332
+ data-action="export-query-chart-png"
333
+ data-chart-id="${escapeHtml(chart.id)}"
334
+ type="button"
335
+ >
336
+ Export PNG
337
+ </button>
338
+ </div>
339
+ </header>
340
+ <div class="query-chart-card__body">
341
+ ${renderChartSurface(chart, state, analysis)}
342
+ </div>
343
+ </article>
344
+ `;
345
+ }
346
+
347
+ function renderChartsDetail(state) {
348
+ const detail = state.charts.detail;
349
+ const selectedHistoryId = state.charts.selectedHistoryId;
350
+
351
+ if (!selectedHistoryId) {
352
+ return renderEmptyChartDetail();
353
+ }
354
+
355
+ if (state.charts.detailLoading && !detail) {
356
+ return `
357
+ <div class="flex flex-1 items-center justify-center px-8 text-center text-on-surface-variant/45">
358
+ <div>
359
+ <span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
360
+ <p class="font-mono text-[10px] uppercase tracking-[0.18em]">Loading Query Detail</p>
361
+ </div>
362
+ </div>
363
+ `;
364
+ }
365
+
366
+ if (state.charts.detailError && !detail) {
367
+ return `
368
+ <div class="p-8">
369
+ <div class="border border-error/30 bg-error-container/20 px-4 py-4 text-sm text-error">
370
+ ${escapeHtml(state.charts.detailError.message)}
371
+ </div>
372
+ </div>
373
+ `;
374
+ }
375
+
376
+ if (!detail?.item) {
377
+ return renderEmptyChartDetail();
378
+ }
379
+
380
+ const analysis = state.charts.result ? analyzeQueryChartResult(state.charts.result) : null;
381
+ const charts = detail.charts ?? [];
382
+
383
+ return `
384
+ <div class="custom-scrollbar flex-1 overflow-auto">
385
+ <div class="charts-detail-shell">
386
+ <header class="charts-detail-shell__header">
387
+ <div class="min-w-0">
388
+ <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/70">
389
+ Charts
390
+ </div>
391
+ <h1 class="mt-2 truncate font-headline text-4xl font-black uppercase tracking-tight text-primary-container">
392
+ ${escapeHtml(detail.item.displayTitle)}
393
+ </h1>
394
+ </div>
395
+ <div class="charts-detail-shell__controls">
396
+ ${renderChartHeightPresetToggle(state)}
397
+ <button
398
+ class="toolbar-button charts-detail-shell__button"
399
+ data-action="open-query-history"
400
+ data-history-id="${escapeHtml(detail.item.id)}"
401
+ type="button"
402
+ >
403
+ Open In Editor
404
+ </button>
405
+ <button
406
+ class="toolbar-button charts-detail-shell__button charts-detail-shell__button--primary"
407
+ data-action="open-create-query-chart-modal"
408
+ type="button"
409
+ ${state.charts.resultError || !state.charts.result ? 'disabled' : ''}
410
+ >
411
+ New Chart
412
+ </button>
413
+ </div>
414
+ </header>
415
+
416
+ ${renderQuerySqlSection(state, detail.item.rawSql)}
417
+
418
+ <section class="mt-6 space-y-5">
419
+ ${
420
+ charts.length
421
+ ? charts.map(chart => renderChartCard(chart, state, analysis)).join('')
422
+ : `
423
+ <div class="flex min-h-[240px] items-center justify-center border border-dashed border-outline-variant/20 bg-surface-container-low px-8 text-center">
424
+ <div>
425
+ <span class="material-symbols-outlined mb-3 text-4xl text-on-surface-variant/25">add_chart</span>
426
+ <p class="font-headline text-lg font-black uppercase tracking-tight text-on-surface">
427
+ No Charts Yet
428
+ </p>
429
+ <p class="mt-2 max-w-md text-sm leading-6 text-on-surface-variant/60">
430
+ Create the first chart for this query. The query stays read-only here; only chart definitions are editable.
431
+ </p>
432
+ </div>
433
+ </div>
434
+ `
435
+ }
436
+ </section>
437
+
438
+ ${renderQueryResultsSection(state)}
439
+ </div>
440
+ </div>
441
+ `;
442
+ }
443
+
444
+ export function renderChartsView(state) {
445
+ if (!state.connections.active && state.charts.error?.code === 'ACTIVE_DATABASE_REQUIRED') {
446
+ return {
447
+ main: `<section class="view-surface flex min-h-full">${renderMissingDatabase()}</section>`,
448
+ panel: '',
449
+ };
450
+ }
451
+
452
+ return {
453
+ main: `
454
+ <section class="charts-view">
455
+ <aside class="charts-view__sidebar">
456
+ <div class="charts-view__sidebar-header">
457
+ <div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
458
+ Query History
459
+ </div>
460
+ <h2 class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
461
+ Charts
462
+ </h2>
463
+ </div>
464
+ ${renderChartsList(state)}
465
+ </aside>
466
+ <div class="charts-view__detail">${renderChartsDetail(state)}</div>
467
+ </section>
468
+ `,
469
+ panel: '',
470
+ };
471
+ }