sqlite-hub 0.6.0 → 0.8.7

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 (44) hide show
  1. package/README.md +107 -0
  2. package/bin/sqlite-hub.js +285 -4
  3. package/changelog.md +21 -0
  4. package/docs/DESIGN_GUIDELINES.md +36 -0
  5. package/frontend/assets/mockups/sql_editor_croped.png +0 -0
  6. package/frontend/index.html +80 -0
  7. package/frontend/js/api.js +34 -0
  8. package/frontend/js/app.js +188 -10
  9. package/frontend/js/components/connectionCard.js +3 -4
  10. package/frontend/js/components/emptyState.js +4 -4
  11. package/frontend/js/components/modal.js +341 -24
  12. package/frontend/js/components/queryChartRenderer.js +125 -0
  13. package/frontend/js/components/queryEditor.js +33 -6
  14. package/frontend/js/components/queryHistoryDetail.js +16 -7
  15. package/frontend/js/components/queryHistoryPanel.js +18 -7
  16. package/frontend/js/components/rowEditorPanel.js +59 -54
  17. package/frontend/js/components/sidebar.js +32 -32
  18. package/frontend/js/components/structureGraph.js +54 -122
  19. package/frontend/js/components/tableDesignerEditor.js +9 -8
  20. package/frontend/js/components/tableDesignerSidebar.js +52 -48
  21. package/frontend/js/components/tableDesignerSqlPreview.js +60 -28
  22. package/frontend/js/lib/queryChartOptions.js +283 -0
  23. package/frontend/js/lib/queryCharts.js +560 -0
  24. package/frontend/js/router.js +8 -0
  25. package/frontend/js/store.js +641 -0
  26. package/frontend/js/views/charts.js +499 -0
  27. package/frontend/js/views/connections.js +5 -17
  28. package/frontend/js/views/data.js +40 -27
  29. package/frontend/js/views/editor.js +19 -13
  30. package/frontend/js/views/overview.js +149 -10
  31. package/frontend/js/views/settings.js +2 -2
  32. package/frontend/js/views/structure.js +105 -59
  33. package/frontend/js/views/tableDesigner.js +7 -2
  34. package/frontend/styles/base.css +62 -0
  35. package/frontend/styles/components.css +68 -118
  36. package/frontend/styles/structure-graph.css +19 -82
  37. package/frontend/styles/tokens.css +2 -0
  38. package/frontend/styles/views.css +293 -66
  39. package/package.json +2 -1
  40. package/server/routes/charts.js +153 -0
  41. package/server/server.js +6 -0
  42. package/server/services/sqlite/overviewService.js +68 -0
  43. package/server/services/storage/appStateStore.js +400 -1
  44. package/server/services/storage/queryHistoryChartUtils.js +145 -0
@@ -4,9 +4,14 @@ import { renderSidebar } from "./components/sidebar.js";
4
4
  import { renderStatusBar } from "./components/statusBar.js";
5
5
  import {
6
6
  mountStructureGraph,
7
- resetPersistedStructureGraphState,
8
7
  teardownStructureGraph,
8
+ resetPersistedStructureGraphState,
9
9
  } from "./components/structureGraph.js";
10
+ import {
11
+ exportQueryChartAsPng,
12
+ mountQueryChartRenderer,
13
+ teardownQueryChartRenderer,
14
+ } from "./components/queryChartRenderer.js";
10
15
  import { renderToasts } from "./components/toast.js";
11
16
  import { renderTopNav } from "./components/topNav.js";
12
17
  import { createRouter } from "./router.js";
@@ -31,22 +36,33 @@ import {
31
36
  openQueryHistoryInEditor,
32
37
  openDeleteDataRowModal,
33
38
  openDeleteEditorRowModal,
39
+ openDeleteQueryChartModal,
34
40
  openEditConnectionModal,
41
+ openCreateQueryChartModal,
42
+ openEditQueryChartModal,
35
43
  refreshCurrentRoute,
36
44
  removeConnection,
37
45
  runQueryHistoryItem,
46
+ saveCurrentQueryChartDraft,
38
47
  selectDataRow,
39
48
  selectEditorRow,
40
49
  selectConnection,
41
50
  selectQueryHistoryItem,
42
51
  selectStructureEntry,
43
52
  setTableDesignerSearchQuery,
53
+ setTableDesignerSqlPreviewVisibility,
54
+ toggleStructureTablesPanel,
44
55
  setDataPage,
45
56
  setDataPageSize,
46
57
  setDataSearchColumn,
47
58
  setDataSearchQuery,
59
+ toggleDataTablesPanel,
48
60
  setCurrentQuery,
61
+ setChartsHeightPreset,
62
+ setEditorPanelVisibility,
49
63
  setEditorTab,
64
+ submitDeleteChartConfirmation,
65
+ setQueryHistoryPanelVisibility,
50
66
  sortDataTableByColumn,
51
67
  sortEditorResultsByColumn,
52
68
  setQueryHistorySearchInput,
@@ -55,6 +71,8 @@ import {
55
71
  saveQueryHistoryNotes,
56
72
  saveQueryHistoryTitle,
57
73
  saveCurrentTableDesignerDraft,
74
+ toggleChartsResultsPanel,
75
+ toggleChartsSqlPanel,
58
76
  queueTableDesignerCsvImport,
59
77
  showToast,
60
78
  submitCreateConnection,
@@ -66,11 +84,14 @@ import {
66
84
  submitOpenConnection,
67
85
  subscribe,
68
86
  toggleQueryHistorySavedState,
87
+ updateCurrentQueryChartDraftConfigField,
88
+ updateCurrentQueryChartDraftField,
69
89
  updateCurrentTableDesignerColumnField,
70
90
  updateCurrentTableDesignerField,
71
91
  addCurrentTableDesignerColumn,
72
92
  removeCurrentTableDesignerColumn,
73
93
  } from "./store.js";
94
+ import { renderChartsView } from "./views/charts.js";
74
95
  import { renderConnectionsView } from "./views/connections.js";
75
96
  import { renderDataView } from "./views/data.js";
76
97
  import { renderEditorView } from "./views/editor.js";
@@ -96,7 +117,21 @@ const shellRefs = {
96
117
  toast: document.querySelector("#toast-root"),
97
118
  };
98
119
  let lastRenderedRoutePath = null;
120
+ let lastRenderedTopNavMarkup = "";
121
+ let lastRenderedSidebarMarkup = "";
122
+ let lastRenderedStatusBarMarkup = "";
123
+ let lastRenderedMainMarkup = "";
124
+ let lastRenderedPanelMarkup = "";
125
+ let lastRenderedModalMarkup = "";
126
+ let lastRenderedToastMarkup = "";
127
+ let lastRenderedPanelOpen = false;
128
+ let lastRenderedLockedRoute = false;
99
129
  let pendingNewTableDesignerAutofocus = false;
130
+ let pendingQueryEditorFocus = false;
131
+
132
+ function invalidateMainRenderCache() {
133
+ lastRenderedMainMarkup = null;
134
+ }
100
135
 
101
136
  function resetStructureGraphForDatabaseChange() {
102
137
  resetPersistedStructureGraphState();
@@ -196,7 +231,7 @@ function renderNotFoundView() {
196
231
  404_SIGNAL
197
232
  </h1>
198
233
  <button
199
- class="mt-8 bg-primary-container px-6 py-3 font-headline text-sm font-bold uppercase tracking-[0.2em] text-on-primary"
234
+ class="standard-button mt-8 px-6 font-headline text-sm"
200
235
  data-action="navigate"
201
236
  data-to="/"
202
237
  type="button"
@@ -218,6 +253,8 @@ function resolveView(state) {
218
253
  return renderConnectionsView(state);
219
254
  case "overview":
220
255
  return renderOverviewView(state);
256
+ case "charts":
257
+ return renderChartsView(state);
221
258
  case "data":
222
259
  return renderDataView(state);
223
260
  case "editor":
@@ -368,6 +405,18 @@ function focusTableDesignerColumnNameField(columnId) {
368
405
  return true;
369
406
  }
370
407
 
408
+ function focusQueryEditorInput() {
409
+ const input = document.querySelector('[data-bind="current-query"]');
410
+
411
+ if (!(input instanceof HTMLTextAreaElement)) {
412
+ return false;
413
+ }
414
+
415
+ input.focus({ preventScroll: true });
416
+ input.setSelectionRange(input.value.length, input.value.length);
417
+ return true;
418
+ }
419
+
371
420
  async function handleTableDesignerCsvImport(fileInput) {
372
421
  if (!(fileInput instanceof HTMLInputElement)) {
373
422
  return;
@@ -408,12 +457,39 @@ async function handleTableDesignerCsvImport(fileInput) {
408
457
  }
409
458
 
410
459
  function renderApp(state) {
411
- const focusedInput = captureFocusedInputState();
412
460
  const previousRoutePath = lastRenderedRoutePath;
413
461
  const { main, panel } = resolveView(state);
414
- const isLockedRoute = ["editor", "editorResults", "data", "structure", "tableDesigner"].includes(
462
+ const topNavMarkup = renderTopNav(state);
463
+ const sidebarMarkup = renderSidebar(state);
464
+ const statusBarMarkup = renderStatusBar(state);
465
+ const modalMarkup = renderModal(state);
466
+ const toastMarkup = renderToasts(state.toasts);
467
+ const isLockedRoute = ["editor", "editorResults", "data", "charts", "structure", "tableDesigner"].includes(
415
468
  state.route.name
416
469
  );
470
+ const panelOpen = Boolean(panel);
471
+ const shellMarkupUnchanged =
472
+ state.route.path === lastRenderedRoutePath &&
473
+ topNavMarkup === lastRenderedTopNavMarkup &&
474
+ sidebarMarkup === lastRenderedSidebarMarkup &&
475
+ statusBarMarkup === lastRenderedStatusBarMarkup &&
476
+ main === lastRenderedMainMarkup &&
477
+ panel === lastRenderedPanelMarkup &&
478
+ modalMarkup === lastRenderedModalMarkup &&
479
+ panelOpen === lastRenderedPanelOpen &&
480
+ isLockedRoute === lastRenderedLockedRoute;
481
+
482
+ if (shellMarkupUnchanged && toastMarkup !== lastRenderedToastMarkup) {
483
+ shellRefs.toast.innerHTML = toastMarkup;
484
+ lastRenderedToastMarkup = toastMarkup;
485
+ return;
486
+ }
487
+
488
+ if (shellMarkupUnchanged && toastMarkup === lastRenderedToastMarkup) {
489
+ return;
490
+ }
491
+
492
+ const focusedInput = captureFocusedInputState();
417
493
  const isEnteringNewTableDesignerRoute =
418
494
  state.route.name === "tableDesigner" &&
419
495
  state.route.params?.isNew &&
@@ -426,17 +502,25 @@ function renderApp(state) {
426
502
  }
427
503
 
428
504
  teardownStructureGraph();
429
- shellRefs.topNav.innerHTML = renderTopNav(state);
430
- shellRefs.sidebar.innerHTML = renderSidebar(state);
431
- shellRefs.statusBar.innerHTML = renderStatusBar(state);
505
+ teardownQueryChartRenderer();
506
+ shellRefs.topNav.innerHTML = topNavMarkup;
507
+ shellRefs.sidebar.innerHTML = sidebarMarkup;
508
+ shellRefs.statusBar.innerHTML = statusBarMarkup;
432
509
  shellRefs.view.innerHTML = main;
433
510
  shellRefs.view.classList.toggle("app-main-scroll--locked", isLockedRoute);
434
511
  shellRefs.panel.innerHTML = panel;
435
- shellRefs.modal.innerHTML = renderModal(state);
436
- shellRefs.toast.innerHTML = renderToasts(state.toasts);
437
- shellRefs.shell.classList.toggle("panel-open", Boolean(panel));
512
+ shellRefs.modal.innerHTML = modalMarkup;
513
+ shellRefs.toast.innerHTML = toastMarkup;
514
+ shellRefs.shell.classList.toggle("panel-open", panelOpen);
438
515
 
439
516
  if (
517
+ pendingQueryEditorFocus &&
518
+ (state.route.name === "editor" || state.route.name === "editorResults")
519
+ ) {
520
+ if (focusQueryEditorInput()) {
521
+ pendingQueryEditorFocus = false;
522
+ }
523
+ } else if (
440
524
  pendingNewTableDesignerAutofocus &&
441
525
  state.route.name === "tableDesigner" &&
442
526
  state.route.params?.isNew &&
@@ -450,12 +534,25 @@ function renderApp(state) {
450
534
  }
451
535
 
452
536
  lastRenderedRoutePath = state.route.path;
537
+ lastRenderedTopNavMarkup = topNavMarkup;
538
+ lastRenderedSidebarMarkup = sidebarMarkup;
539
+ lastRenderedStatusBarMarkup = statusBarMarkup;
540
+ lastRenderedMainMarkup = main;
541
+ lastRenderedPanelMarkup = panel;
542
+ lastRenderedModalMarkup = modalMarkup;
543
+ lastRenderedToastMarkup = toastMarkup;
544
+ lastRenderedPanelOpen = panelOpen;
545
+ lastRenderedLockedRoute = isLockedRoute;
453
546
 
454
547
  if (state.route.name === "structure") {
455
548
  mountStructureGraph(state).catch((error) => {
456
549
  console.error("Failed to mount structure graph.", error);
457
550
  });
458
551
  }
552
+
553
+ if (state.route.name === "charts") {
554
+ mountQueryChartRenderer(state);
555
+ }
459
556
  }
460
557
 
461
558
  const router = createRouter((route) => {
@@ -480,6 +577,15 @@ async function handleAction(actionNode) {
480
577
  case "open-modal":
481
578
  openModal(actionNode.dataset.modal);
482
579
  return;
580
+ case "open-create-query-chart-modal":
581
+ openCreateQueryChartModal();
582
+ return;
583
+ case "open-edit-query-chart-modal":
584
+ openEditQueryChartModal(actionNode.dataset.chartId);
585
+ return;
586
+ case "open-delete-query-chart-modal":
587
+ openDeleteQueryChartModal(actionNode.dataset.chartId);
588
+ return;
483
589
  case "edit-connection":
484
590
  openEditConnectionModal(actionNode.dataset.connectionId);
485
591
  return;
@@ -532,6 +638,7 @@ async function handleAction(actionNode) {
532
638
  openDeleteEditorRowModal(actionNode.dataset.rowIndex);
533
639
  return;
534
640
  case "clear-query":
641
+ pendingQueryEditorFocus = true;
535
642
  clearCurrentQuery();
536
643
  if (getState().route.name === "editorResults") {
537
644
  router.navigate("/editor");
@@ -554,6 +661,16 @@ async function handleAction(actionNode) {
554
661
  await setQueryHistoryTab(actionNode.dataset.tab);
555
662
  }
556
663
  return;
664
+ case "toggle-query-history-panel":
665
+ setQueryHistoryPanelVisibility(
666
+ actionNode.dataset.nextValue ? actionNode.dataset.nextValue === "true" : undefined
667
+ );
668
+ return;
669
+ case "toggle-editor-panel":
670
+ setEditorPanelVisibility(
671
+ actionNode.dataset.nextValue ? actionNode.dataset.nextValue === "true" : undefined
672
+ );
673
+ return;
557
674
  case "load-more-query-history":
558
675
  await loadMoreQueryHistory();
559
676
  return;
@@ -576,6 +693,22 @@ async function handleAction(actionNode) {
576
693
  );
577
694
  }
578
695
  return;
696
+ case "toggle-charts-sql-panel":
697
+ toggleChartsSqlPanel();
698
+ return;
699
+ case "toggle-charts-results-panel":
700
+ toggleChartsResultsPanel();
701
+ return;
702
+ case "set-charts-height-preset":
703
+ if (actionNode.dataset.preset) {
704
+ setChartsHeightPreset(actionNode.dataset.preset);
705
+ }
706
+ return;
707
+ case "export-query-chart-png":
708
+ if (actionNode.dataset.chartId && !exportQueryChartAsPng(actionNode.dataset.chartId)) {
709
+ showToast("The selected chart is not ready for PNG export.", "alert");
710
+ }
711
+ return;
579
712
  case "delete-query-history":
580
713
  if (
581
714
  actionNode.dataset.historyId &&
@@ -599,6 +732,12 @@ async function handleAction(actionNode) {
599
732
  case "export-data-csv":
600
733
  await exportCurrentDataTableCsv();
601
734
  return;
735
+ case "toggle-data-tables":
736
+ toggleDataTablesPanel();
737
+ return;
738
+ case "toggle-structure-tables":
739
+ toggleStructureTablesPanel();
740
+ return;
602
741
  case "select-structure-entry":
603
742
  if (actionNode.dataset.entryName) {
604
743
  await selectStructureEntry(actionNode.dataset.entryName);
@@ -640,6 +779,11 @@ async function handleAction(actionNode) {
640
779
  }
641
780
  return;
642
781
  }
782
+ case "toggle-table-designer-sql-preview":
783
+ setTableDesignerSqlPreviewVisibility(
784
+ actionNode.dataset.nextValue ? actionNode.dataset.nextValue === "true" : undefined
785
+ );
786
+ return;
643
787
  case "import-table-designer-csv": {
644
788
  const fileInput = document.querySelector('[data-bind="table-designer-import-file"]');
645
789
 
@@ -758,6 +902,7 @@ document.addEventListener("input", (event) => {
758
902
  }
759
903
 
760
904
  if (bindNode.dataset.bind === "current-query") {
905
+ invalidateMainRenderCache();
761
906
  syncQueryEditorHighlight(bindNode);
762
907
  syncQueryEditorScroll(bindNode);
763
908
  setCurrentQuery(bindNode.value);
@@ -794,6 +939,11 @@ document.addEventListener("input", (event) => {
794
939
 
795
940
  if (bindNode.dataset.bind === "query-history-search") {
796
941
  setQueryHistorySearchInput(bindNode.value);
942
+ return;
943
+ }
944
+
945
+ if (bindNode.dataset.bind === "query-chart-draft:name") {
946
+ updateCurrentQueryChartDraftField("name", bindNode.value);
797
947
  }
798
948
  });
799
949
 
@@ -853,6 +1003,28 @@ document.addEventListener("change", (event) => {
853
1003
  bindNode.dataset.field,
854
1004
  bindNode.checked
855
1005
  );
1006
+ return;
1007
+ }
1008
+
1009
+ if (bindNode.dataset.bind === "query-chart-draft:chartType") {
1010
+ updateCurrentQueryChartDraftField("chartType", bindNode.value);
1011
+ return;
1012
+ }
1013
+
1014
+ if (bindNode.dataset.bind === "query-chart-draft:tableVisible") {
1015
+ updateCurrentQueryChartDraftField("tableVisible", bindNode.checked);
1016
+ return;
1017
+ }
1018
+
1019
+ if (bindNode.dataset.bind?.startsWith("query-chart-draft-config:")) {
1020
+ const field = bindNode.dataset.bind.slice("query-chart-draft-config:".length);
1021
+ const value =
1022
+ bindNode instanceof HTMLInputElement && bindNode.type === "checkbox"
1023
+ ? bindNode.checked
1024
+ : bindNode.value === ""
1025
+ ? null
1026
+ : bindNode.value;
1027
+ updateCurrentQueryChartDraftConfigField(field, value);
856
1028
  }
857
1029
  });
858
1030
 
@@ -938,6 +1110,12 @@ document.addEventListener("submit", async (event) => {
938
1110
  case "delete-row-confirm":
939
1111
  await submitDeleteRowConfirmation();
940
1112
  return;
1113
+ case "save-query-chart":
1114
+ await saveCurrentQueryChartDraft();
1115
+ return;
1116
+ case "delete-query-chart":
1117
+ await submitDeleteChartConfirmation();
1118
+ return;
941
1119
  case "save-data-row": {
942
1120
  const values = {};
943
1121
 
@@ -73,17 +73,16 @@ export function renderConnectionCard(connection, activeConnectionId) {
73
73
  <div class="border-t border-outline-variant/10 bg-surface-container-low px-4 py-3">
74
74
  <div class="grid grid-cols-[minmax(0,1fr)_5.1rem_5.8rem] gap-2">
75
75
  <button
76
- class="clipped-corner min-w-0 bg-primary-container px-3 py-2.5 text-[10px] font-black uppercase tracking-[0.18em] text-on-primary shadow-[0_0_18px_-10px_rgba(252,227,0,0.7)] transition-[filter,box-shadow] hover:brightness-105"
76
+ class="signature-button"
77
77
  data-action="select-connection"
78
78
  data-connection-id="${escapeHtml(connection.id)}"
79
- style="--clip-path: ${clipPath};"
80
79
  type="button"
81
80
  title="${primaryActionLabel}"
82
81
  >
83
82
  ${primaryActionLabel}
84
83
  </button>
85
84
  <button
86
- class="border border-outline-variant/20 bg-surface-container px-3 py-2.5 text-[10px] font-bold uppercase tracking-[0.18em] text-on-surface-variant transition-colors hover:bg-surface-container-highest"
85
+ class="standard-button"
87
86
  data-action="edit-connection"
88
87
  data-connection-id="${escapeHtml(connection.id)}"
89
88
  type="button"
@@ -91,7 +90,7 @@ export function renderConnectionCard(connection, activeConnectionId) {
91
90
  Edit
92
91
  </button>
93
92
  <button
94
- class="border border-outline-variant/20 bg-surface-container px-3 py-2.5 text-[10px] font-bold uppercase tracking-[0.18em] text-on-surface-variant transition-colors hover:bg-surface-container-highest"
93
+ class="delete-button"
95
94
  data-action="remove-connection"
96
95
  data-connection-id="${escapeHtml(connection.id)}"
97
96
  type="button"
@@ -22,7 +22,7 @@ function renderRecentConnections(recentConnections = []) {
22
22
  .map(
23
23
  (connection) => `
24
24
  <button
25
- class="flex items-center gap-2 border border-outline-variant/15 bg-surface-container-low px-4 py-3 text-left text-on-surface transition-colors hover:border-primary-container/30 hover:bg-surface-container-high"
25
+ class="control-button flex items-center gap-2 border border-outline-variant/15 bg-surface-container-low px-4 text-left text-on-surface transition-colors hover:border-primary-container/30 hover:bg-surface-container-high"
26
26
  data-action="select-connection"
27
27
  data-connection-id="${escapeHtml(connection.id)}"
28
28
  type="button"
@@ -135,7 +135,7 @@ export function renderEmptyState({ activeConnection, recentConnections = [] }) {
135
135
  ? `
136
136
  <div class="mx-auto mt-8 grid w-full max-w-3xl grid-cols-1 gap-4 px-6 md:grid-cols-3">
137
137
  <button
138
- class="border border-outline-variant/20 bg-surface-container-low px-5 py-4 font-headline text-sm font-bold uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container/30 hover:text-primary-container"
138
+ class="standard-button"
139
139
  data-action="navigate"
140
140
  data-to="/overview"
141
141
  type="button"
@@ -143,7 +143,7 @@ export function renderEmptyState({ activeConnection, recentConnections = [] }) {
143
143
  Overview
144
144
  </button>
145
145
  <button
146
- class="border border-outline-variant/20 bg-surface-container-low px-5 py-4 font-headline text-sm font-bold uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container/30 hover:text-primary-container"
146
+ class="standard-button"
147
147
  data-action="navigate"
148
148
  data-to="/structure"
149
149
  type="button"
@@ -151,7 +151,7 @@ export function renderEmptyState({ activeConnection, recentConnections = [] }) {
151
151
  Structure
152
152
  </button>
153
153
  <button
154
- class="border border-outline-variant/20 bg-surface-container-low px-5 py-4 font-headline text-sm font-bold uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container/30 hover:text-primary-container"
154
+ class="standard-button"
155
155
  data-action="navigate"
156
156
  data-to="/editor"
157
157
  type="button"