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
@@ -9,6 +9,15 @@ import {
9
9
  updateTableDesignerColumnField,
10
10
  updateTableDesignerDraftField,
11
11
  } from "./utils/tableDesigner.js";
12
+ import {
13
+ analyzeQueryChartResult,
14
+ buildDefaultQueryChartName,
15
+ buildQueryChartResultColumns,
16
+ buildSuggestedChartConfig,
17
+ resolveUniqueQueryChartName,
18
+ suggestQueryChartType,
19
+ validateQueryChartConfig,
20
+ } from "./lib/queryCharts.js";
12
21
 
13
22
  const listeners = new Set();
14
23
  const DEFAULT_SETTINGS = {
@@ -19,6 +28,7 @@ const DEFAULT_SETTINGS = {
19
28
  const DATA_PAGE_SIZES = [25, 50, 100];
20
29
  const QUERY_HISTORY_PAGE_SIZE = 30;
21
30
  const QUERY_HISTORY_RUN_LIMIT = 8;
31
+ const CHART_HEIGHT_PRESETS = new Set(["small", "medium", "large"]);
22
32
  const MISSING_DATABASE_ERROR = {
23
33
  code: "ACTIVE_DATABASE_REQUIRED",
24
34
  message: "No active SQLite database selected.",
@@ -28,6 +38,8 @@ let routeLoadVersion = 0;
28
38
  let queryHistoryLoadVersion = 0;
29
39
  let queryHistoryDetailLoadVersion = 0;
30
40
  let queryHistorySearchTimer = null;
41
+ let chartsLoadVersion = 0;
42
+ let chartsDetailLoadVersion = 0;
31
43
 
32
44
  const state = {
33
45
  ready: false,
@@ -55,6 +67,7 @@ const state = {
55
67
  dataBrowser: {
56
68
  tables: [],
57
69
  selectedTable: null,
70
+ tablesVisible: true,
58
71
  table: null,
59
72
  loading: false,
60
73
  tableLoading: false,
@@ -73,7 +86,9 @@ const state = {
73
86
  },
74
87
  editor: {
75
88
  sqlText: "",
89
+ editorPanelVisible: true,
76
90
  history: [],
91
+ historyPanelVisible: true,
77
92
  historyLoading: false,
78
93
  historyLoadingMore: false,
79
94
  historyError: null,
@@ -101,10 +116,26 @@ const state = {
101
116
  deleting: false,
102
117
  saveError: null,
103
118
  },
119
+ charts: {
120
+ queries: [],
121
+ loading: false,
122
+ error: null,
123
+ selectedHistoryId: null,
124
+ chartHeightPreset: "medium",
125
+ sqlExpanded: false,
126
+ resultsVisible: true,
127
+ detail: null,
128
+ detailLoading: false,
129
+ detailError: null,
130
+ result: null,
131
+ resultLoading: false,
132
+ resultError: null,
133
+ },
104
134
  tableDesigner: {
105
135
  tables: [],
106
136
  selectedTableName: null,
107
137
  draft: null,
138
+ sqlPreviewVisible: true,
108
139
  pendingImportedDraft: null,
109
140
  loading: false,
110
141
  detailLoading: false,
@@ -118,6 +149,7 @@ const state = {
118
149
  data: null,
119
150
  selectedName: null,
120
151
  detail: null,
152
+ tablesVisible: true,
121
153
  loading: false,
122
154
  detailLoading: false,
123
155
  error: null,
@@ -172,6 +204,7 @@ function requiresActiveDatabase(routeName) {
172
204
  "data",
173
205
  "editor",
174
206
  "editorResults",
207
+ "charts",
175
208
  "structure",
176
209
  "tableDesigner",
177
210
  ].includes(routeName);
@@ -375,6 +408,52 @@ function resetQueryHistoryState({ preserveSearch = true } = {}) {
375
408
  }
376
409
  }
377
410
 
411
+ function resetChartsState() {
412
+ chartsLoadVersion += 1;
413
+ chartsDetailLoadVersion += 1;
414
+ state.charts.queries = [];
415
+ state.charts.loading = false;
416
+ state.charts.error = null;
417
+ state.charts.selectedHistoryId = null;
418
+ state.charts.chartHeightPreset = "medium";
419
+ state.charts.sqlExpanded = false;
420
+ state.charts.resultsVisible = true;
421
+ state.charts.detail = null;
422
+ state.charts.detailLoading = false;
423
+ state.charts.detailError = null;
424
+ state.charts.result = null;
425
+ state.charts.resultLoading = false;
426
+ state.charts.resultError = null;
427
+ }
428
+
429
+ function normalizeChartsHeightPreset(value) {
430
+ const normalizedValue = String(value ?? "")
431
+ .trim()
432
+ .toLowerCase();
433
+
434
+ return CHART_HEIGHT_PRESETS.has(normalizedValue) ? normalizedValue : "medium";
435
+ }
436
+
437
+ function mergeQueryHistoryItemWithChartSummary(updatedItem, fallbackItem = null) {
438
+ if (!updatedItem) {
439
+ return updatedItem;
440
+ }
441
+
442
+ return {
443
+ ...(fallbackItem ?? {}),
444
+ ...updatedItem,
445
+ chartCount:
446
+ updatedItem.chartCount !== undefined
447
+ ? Number(updatedItem.chartCount ?? 0)
448
+ : Number(fallbackItem?.chartCount ?? 0),
449
+ chartTypes: Array.isArray(updatedItem.chartTypes)
450
+ ? [...updatedItem.chartTypes]
451
+ : Array.isArray(fallbackItem?.chartTypes)
452
+ ? [...fallbackItem.chartTypes]
453
+ : [],
454
+ };
455
+ }
456
+
378
457
  function syncQueryHistoryItem(updatedItem) {
379
458
  if (!updatedItem) {
380
459
  return;
@@ -387,6 +466,61 @@ function syncQueryHistoryItem(updatedItem) {
387
466
  if (state.editor.historyDetail?.id === updatedItem.id) {
388
467
  state.editor.historyDetail = updatedItem;
389
468
  }
469
+
470
+ const existingChartsEntry =
471
+ state.charts.queries.find((entry) => entry.id === updatedItem.id) ??
472
+ (state.charts.detail?.item?.id === updatedItem.id ? state.charts.detail.item : null);
473
+ const mergedChartsItem = mergeQueryHistoryItemWithChartSummary(updatedItem, existingChartsEntry);
474
+
475
+ state.charts.queries = state.charts.queries.map((entry) =>
476
+ entry.id === updatedItem.id ? mergedChartsItem : entry
477
+ );
478
+
479
+ if (state.charts.detail?.item?.id === updatedItem.id) {
480
+ state.charts.detail = {
481
+ ...state.charts.detail,
482
+ item: mergeQueryHistoryItemWithChartSummary(updatedItem, state.charts.detail.item),
483
+ };
484
+ }
485
+ }
486
+
487
+ function syncChartsQuerySummaryForHistory(historyId) {
488
+ const numericId = Number(historyId);
489
+
490
+ if (!Number.isInteger(numericId) || numericId < 1 || state.charts.detail?.item?.id !== numericId) {
491
+ return;
492
+ }
493
+
494
+ const charts = state.charts.detail?.charts ?? [];
495
+ const chartTypes = [...new Set(
496
+ charts
497
+ .map((chart) =>
498
+ String(chart?.chartType ?? "")
499
+ .trim()
500
+ .toLowerCase()
501
+ )
502
+ .filter(Boolean)
503
+ )];
504
+ const chartCount = charts.length;
505
+
506
+ state.charts.queries = state.charts.queries.map((entry) =>
507
+ entry.id === numericId
508
+ ? {
509
+ ...entry,
510
+ chartCount,
511
+ chartTypes,
512
+ }
513
+ : entry
514
+ );
515
+
516
+ state.charts.detail = {
517
+ ...state.charts.detail,
518
+ item: {
519
+ ...state.charts.detail.item,
520
+ chartCount,
521
+ chartTypes,
522
+ },
523
+ };
390
524
  }
391
525
 
392
526
  function resolveQueryHistorySql(historyId) {
@@ -396,6 +530,10 @@ function resolveQueryHistorySql(historyId) {
396
530
  return state.editor.historyDetail.rawSql;
397
531
  }
398
532
 
533
+ if (String(state.charts.detail?.item?.id ?? "") === historyIdAsString) {
534
+ return state.charts.detail.item.rawSql;
535
+ }
536
+
399
537
  return findQueryHistoryItem(historyId)?.rawSql ?? null;
400
538
  }
401
539
 
@@ -403,6 +541,9 @@ function clearRouteSlices() {
403
541
  state.overview.error = null;
404
542
  state.dataBrowser.error = null;
405
543
  state.dataBrowser.saveError = null;
544
+ state.charts.error = null;
545
+ state.charts.detailError = null;
546
+ state.charts.resultError = null;
406
547
  state.tableDesigner.error = null;
407
548
  state.tableDesigner.saveError = null;
408
549
  state.structure.error = null;
@@ -432,6 +573,7 @@ function setMissingDatabaseState() {
432
573
  state.structure.detailLoading = false;
433
574
  state.structure.data = null;
434
575
  state.structure.detail = null;
576
+ state.structure.tablesVisible = true;
435
577
  state.structure.error = error;
436
578
 
437
579
  state.tableDesigner.loading = false;
@@ -439,6 +581,7 @@ function setMissingDatabaseState() {
439
581
  state.tableDesigner.tables = [];
440
582
  state.tableDesigner.selectedTableName = null;
441
583
  state.tableDesigner.draft = null;
584
+ state.tableDesigner.sqlPreviewVisible = true;
442
585
  state.tableDesigner.pendingImportedDraft = null;
443
586
  state.tableDesigner.saving = false;
444
587
  state.tableDesigner.searchQuery = "";
@@ -446,6 +589,9 @@ function setMissingDatabaseState() {
446
589
  state.tableDesigner.error = error;
447
590
  state.tableDesigner.saveError = null;
448
591
 
592
+ resetChartsState();
593
+ state.charts.error = error;
594
+
449
595
  resetQueryHistoryState({ preserveSearch: false });
450
596
  }
451
597
 
@@ -658,6 +804,114 @@ async function refreshQueryHistoryState({ append = false } = {}) {
658
804
  }
659
805
  }
660
806
 
807
+ async function loadChartsDetail(historyId) {
808
+ const numericId = Number(historyId);
809
+ const requestVersion = ++chartsDetailLoadVersion;
810
+
811
+ if (!Number.isInteger(numericId) || numericId < 1) {
812
+ state.charts.selectedHistoryId = null;
813
+ state.charts.sqlExpanded = false;
814
+ state.charts.resultsVisible = true;
815
+ state.charts.detail = null;
816
+ state.charts.detailLoading = false;
817
+ state.charts.detailError = null;
818
+ state.charts.result = null;
819
+ state.charts.resultLoading = false;
820
+ state.charts.resultError = null;
821
+ emitChange();
822
+ return;
823
+ }
824
+
825
+ state.charts.selectedHistoryId = numericId;
826
+ state.charts.sqlExpanded = false;
827
+ state.charts.resultsVisible = true;
828
+ state.charts.detail = null;
829
+ state.charts.detailLoading = true;
830
+ state.charts.detailError = null;
831
+ state.charts.result = null;
832
+ state.charts.resultLoading = true;
833
+ state.charts.resultError = null;
834
+ emitChange();
835
+
836
+ const [detailResponse, resultResponse] = await Promise.allSettled([
837
+ api.getChartsQueryHistoryDetail(numericId),
838
+ api.executeChartsQueryHistory(numericId),
839
+ ]);
840
+
841
+ if (
842
+ requestVersion !== chartsDetailLoadVersion ||
843
+ state.charts.selectedHistoryId !== numericId
844
+ ) {
845
+ return;
846
+ }
847
+
848
+ if (detailResponse.status === "fulfilled") {
849
+ state.charts.detail = detailResponse.value.data ?? null;
850
+ state.charts.detailError = null;
851
+ if (detailResponse.value.data?.item) {
852
+ syncQueryHistoryItem(detailResponse.value.data.item);
853
+ syncChartsQuerySummaryForHistory(detailResponse.value.data.item.id);
854
+ }
855
+ } else {
856
+ state.charts.detail = null;
857
+ state.charts.detailError = normalizeError(detailResponse.reason);
858
+ }
859
+
860
+ if (resultResponse.status === "fulfilled") {
861
+ state.charts.result = resultResponse.value.data ?? null;
862
+ state.charts.resultError = null;
863
+ } else {
864
+ state.charts.result = null;
865
+ state.charts.resultError = normalizeError(resultResponse.reason);
866
+ }
867
+
868
+ if (requestVersion === chartsDetailLoadVersion) {
869
+ state.charts.detailLoading = false;
870
+ state.charts.resultLoading = false;
871
+ emitChange();
872
+ }
873
+ }
874
+
875
+ async function loadCharts(version, route) {
876
+ state.charts.loading = true;
877
+ state.charts.error = null;
878
+ emitChange();
879
+
880
+ try {
881
+ const response = await api.getChartsQueryHistory();
882
+
883
+ if (version !== routeLoadVersion) {
884
+ return;
885
+ }
886
+
887
+ state.charts.queries = response.data ?? [];
888
+ state.charts.error = null;
889
+ } catch (error) {
890
+ if (version !== routeLoadVersion) {
891
+ return;
892
+ }
893
+
894
+ state.charts.queries = [];
895
+ state.charts.error = normalizeError(error);
896
+ } finally {
897
+ if (version === routeLoadVersion) {
898
+ state.charts.loading = false;
899
+ emitChange();
900
+ }
901
+ }
902
+
903
+ if (version !== routeLoadVersion) {
904
+ return;
905
+ }
906
+
907
+ const requestedHistoryId = Number(route.params?.historyId ?? null);
908
+ const canLoadRequestedHistory =
909
+ Number.isInteger(requestedHistoryId) &&
910
+ state.charts.queries.some((item) => item.id === requestedHistoryId);
911
+
912
+ await loadChartsDetail(canLoadRequestedHistory ? requestedHistoryId : null);
913
+ }
914
+
661
915
  async function loadOverview(version) {
662
916
  state.overview.loading = true;
663
917
  state.overview.error = null;
@@ -998,14 +1252,17 @@ function invalidateDatabaseCaches() {
998
1252
  state.tableDesigner.tables = [];
999
1253
  state.tableDesigner.selectedTableName = null;
1000
1254
  state.tableDesigner.draft = null;
1255
+ state.tableDesigner.sqlPreviewVisible = true;
1001
1256
  state.tableDesigner.pendingImportedDraft = null;
1002
1257
  state.tableDesigner.saving = false;
1003
1258
  state.tableDesigner.searchQuery = "";
1004
1259
  state.tableDesigner.supportedTypes = [];
1005
1260
  state.tableDesigner.error = null;
1006
1261
  state.tableDesigner.saveError = null;
1262
+ resetChartsState();
1007
1263
  state.structure.data = null;
1008
1264
  state.structure.detail = null;
1265
+ state.structure.tablesVisible = true;
1009
1266
  }
1010
1267
 
1011
1268
  async function loadRouteData(route) {
@@ -1031,6 +1288,9 @@ async function loadRouteData(route) {
1031
1288
  case "data":
1032
1289
  await loadData(version, route);
1033
1290
  return;
1291
+ case "charts":
1292
+ await loadCharts(version, route);
1293
+ return;
1034
1294
  case "editor":
1035
1295
  case "editorResults":
1036
1296
  await refreshQueryHistoryState();
@@ -1083,6 +1343,94 @@ function closeModalInternal() {
1083
1343
  emitChange();
1084
1344
  }
1085
1345
 
1346
+ function getChartsResultAnalysis(snapshot = state) {
1347
+ return snapshot.charts.result ? analyzeQueryChartResult(snapshot.charts.result) : null;
1348
+ }
1349
+
1350
+ function buildQueryChartDraft(mode, chart = null) {
1351
+ const queryItem = state.charts.detail?.item ?? null;
1352
+ const analysis = getChartsResultAnalysis();
1353
+
1354
+ if (!queryItem || !analysis) {
1355
+ return null;
1356
+ }
1357
+
1358
+ const existingCharts = state.charts.detail?.charts ?? [];
1359
+
1360
+ if (mode === "edit" && chart) {
1361
+ return {
1362
+ mode,
1363
+ chartId: chart.id,
1364
+ queryHistoryId: chart.queryHistoryId,
1365
+ chartType: chart.chartType,
1366
+ name: chart.name,
1367
+ nameTouched: true,
1368
+ config: structuredClone(chart.config),
1369
+ tableVisible: chart.tableVisible,
1370
+ resultColumns: buildQueryChartResultColumns(analysis),
1371
+ };
1372
+ }
1373
+
1374
+ const chartType = suggestQueryChartType(analysis);
1375
+ const name = resolveUniqueQueryChartName(
1376
+ buildDefaultQueryChartName(chartType, queryItem.displayTitle),
1377
+ existingCharts
1378
+ );
1379
+
1380
+ return {
1381
+ mode: "create",
1382
+ chartId: null,
1383
+ queryHistoryId: queryItem.id,
1384
+ chartType,
1385
+ name,
1386
+ nameTouched: false,
1387
+ config: buildSuggestedChartConfig(chartType, analysis),
1388
+ tableVisible: true,
1389
+ resultColumns: buildQueryChartResultColumns(analysis),
1390
+ };
1391
+ }
1392
+
1393
+ function upsertChartDetailItem(updatedChart) {
1394
+ if (!updatedChart || state.charts.detail?.item?.id !== updatedChart.queryHistoryId) {
1395
+ return;
1396
+ }
1397
+
1398
+ const nextCharts = [...(state.charts.detail?.charts ?? [])];
1399
+ const existingIndex = nextCharts.findIndex((chart) => chart.id === updatedChart.id);
1400
+
1401
+ if (existingIndex >= 0) {
1402
+ nextCharts.splice(existingIndex, 1, updatedChart);
1403
+ } else {
1404
+ nextCharts.push(updatedChart);
1405
+ }
1406
+
1407
+ nextCharts.sort((left, right) => {
1408
+ const leftTime = Date.parse(left.createdAt ?? "") || 0;
1409
+ const rightTime = Date.parse(right.createdAt ?? "") || 0;
1410
+ return leftTime - rightTime || left.id - right.id;
1411
+ });
1412
+
1413
+ state.charts.detail = {
1414
+ ...state.charts.detail,
1415
+ charts: nextCharts,
1416
+ };
1417
+
1418
+ syncChartsQuerySummaryForHistory(updatedChart.queryHistoryId);
1419
+ }
1420
+
1421
+ function removeChartDetailItem(chartId) {
1422
+ if (!state.charts.detail) {
1423
+ return;
1424
+ }
1425
+
1426
+ state.charts.detail = {
1427
+ ...state.charts.detail,
1428
+ charts: (state.charts.detail.charts ?? []).filter((chart) => chart.id !== Number(chartId)),
1429
+ };
1430
+
1431
+ syncChartsQuerySummaryForHistory(state.charts.detail.item?.id);
1432
+ }
1433
+
1086
1434
  export function getState() {
1087
1435
  return clone(state);
1088
1436
  }
@@ -1205,10 +1553,147 @@ export function openDeleteEditorRowModal(rowIndex) {
1205
1553
  emitChange();
1206
1554
  }
1207
1555
 
1556
+ export function openCreateQueryChartModal() {
1557
+ const draft = buildQueryChartDraft("create");
1558
+
1559
+ if (!state.charts.detail?.item || !state.charts.result) {
1560
+ pushToast("The selected query has no chartable result set yet.", "alert");
1561
+ return;
1562
+ }
1563
+
1564
+ if (!draft) {
1565
+ pushToast("The chart editor could not be opened for this query.", "alert");
1566
+ return;
1567
+ }
1568
+
1569
+ state.modal = {
1570
+ kind: "chart-editor",
1571
+ error: null,
1572
+ submitting: false,
1573
+ draft,
1574
+ };
1575
+ emitChange();
1576
+ }
1577
+
1578
+ export function openEditQueryChartModal(chartId) {
1579
+ if (!state.charts.result) {
1580
+ pushToast("Reload the query result before editing this chart.", "alert");
1581
+ return;
1582
+ }
1583
+
1584
+ const chart = state.charts.detail?.charts?.find((entry) => entry.id === Number(chartId)) ?? null;
1585
+ const draft = chart ? buildQueryChartDraft("edit", chart) : null;
1586
+
1587
+ if (!chart || !draft) {
1588
+ pushToast("The selected chart could not be loaded.", "alert");
1589
+ return;
1590
+ }
1591
+
1592
+ state.modal = {
1593
+ kind: "chart-editor",
1594
+ error: null,
1595
+ submitting: false,
1596
+ draft,
1597
+ };
1598
+ emitChange();
1599
+ }
1600
+
1601
+ export function openDeleteQueryChartModal(chartId) {
1602
+ const chart = state.charts.detail?.charts?.find((entry) => entry.id === Number(chartId)) ?? null;
1603
+
1604
+ if (!chart) {
1605
+ pushToast("The selected chart could not be loaded.", "alert");
1606
+ return;
1607
+ }
1608
+
1609
+ state.modal = {
1610
+ kind: "delete-chart",
1611
+ chartId: chart.id,
1612
+ chartName: chart.name,
1613
+ error: null,
1614
+ submitting: false,
1615
+ };
1616
+ emitChange();
1617
+ }
1618
+
1208
1619
  export function closeModal() {
1209
1620
  closeModalInternal();
1210
1621
  }
1211
1622
 
1623
+ export function toggleChartsSqlPanel() {
1624
+ state.charts.sqlExpanded = !state.charts.sqlExpanded;
1625
+ emitChange();
1626
+ }
1627
+
1628
+ export function toggleChartsResultsPanel() {
1629
+ state.charts.resultsVisible = !state.charts.resultsVisible;
1630
+ emitChange();
1631
+ }
1632
+
1633
+ export function setChartsHeightPreset(preset) {
1634
+ const nextPreset = normalizeChartsHeightPreset(preset);
1635
+
1636
+ if (state.charts.chartHeightPreset === nextPreset) {
1637
+ return;
1638
+ }
1639
+
1640
+ state.charts.chartHeightPreset = nextPreset;
1641
+ emitChange();
1642
+ }
1643
+
1644
+ export function updateCurrentQueryChartDraftField(field, value) {
1645
+ if (state.modal?.kind !== "chart-editor" || !state.modal.draft) {
1646
+ return;
1647
+ }
1648
+
1649
+ state.modal.error = null;
1650
+
1651
+ if (field === "name") {
1652
+ state.modal.draft.name = String(value ?? "");
1653
+ state.modal.draft.nameTouched = true;
1654
+ emitChange();
1655
+ return;
1656
+ }
1657
+
1658
+ if (field === "chartType") {
1659
+ const nextType = String(value ?? "").trim().toLowerCase();
1660
+ const analysis = getChartsResultAnalysis();
1661
+
1662
+ state.modal.draft.chartType = nextType;
1663
+ state.modal.draft.config = buildSuggestedChartConfig(nextType, analysis);
1664
+
1665
+ if (!state.modal.draft.nameTouched) {
1666
+ const existingCharts = state.charts.detail?.charts ?? [];
1667
+ state.modal.draft.name = resolveUniqueQueryChartName(
1668
+ buildDefaultQueryChartName(nextType, state.charts.detail?.item?.displayTitle),
1669
+ existingCharts,
1670
+ state.modal.draft.chartId
1671
+ );
1672
+ }
1673
+
1674
+ emitChange();
1675
+ return;
1676
+ }
1677
+
1678
+ if (field === "tableVisible") {
1679
+ state.modal.draft.tableVisible = Boolean(value);
1680
+ emitChange();
1681
+ }
1682
+ }
1683
+
1684
+ export function updateCurrentQueryChartDraftConfigField(field, value) {
1685
+ if (state.modal?.kind !== "chart-editor" || !state.modal.draft) {
1686
+ return;
1687
+ }
1688
+
1689
+ state.modal.error = null;
1690
+ state.modal.draft.config = {
1691
+ ...state.modal.draft.config,
1692
+ [field]: value,
1693
+ };
1694
+ emitChange();
1695
+ }
1696
+
1212
1697
  export function dismissToast(id) {
1213
1698
  const nextToasts = state.toasts.filter((toast) => toast.id !== id);
1214
1699
 
@@ -1408,6 +1893,18 @@ export function clearEditorResults() {
1408
1893
  emitChange();
1409
1894
  }
1410
1895
 
1896
+ export function setEditorPanelVisibility(visible) {
1897
+ const nextValue =
1898
+ typeof visible === "boolean" ? visible : !Boolean(state.editor.editorPanelVisible);
1899
+
1900
+ if (state.editor.editorPanelVisible === nextValue) {
1901
+ return;
1902
+ }
1903
+
1904
+ state.editor.editorPanelVisible = nextValue;
1905
+ emitChange();
1906
+ }
1907
+
1411
1908
  export function setEditorTab(tab) {
1412
1909
  state.editor.activeTab = tab;
1413
1910
  emitChange();
@@ -1478,6 +1975,18 @@ export async function setQueryHistoryTab(tab) {
1478
1975
  await refreshQueryHistoryState();
1479
1976
  }
1480
1977
 
1978
+ export function setQueryHistoryPanelVisibility(visible) {
1979
+ const nextValue =
1980
+ typeof visible === "boolean" ? visible : !Boolean(state.editor.historyPanelVisible);
1981
+
1982
+ if (state.editor.historyPanelVisible === nextValue) {
1983
+ return;
1984
+ }
1985
+
1986
+ state.editor.historyPanelVisible = nextValue;
1987
+ emitChange();
1988
+ }
1989
+
1481
1990
  export function setQueryHistorySearchInput(query) {
1482
1991
  state.editor.historySearchInput = String(query ?? "");
1483
1992
  emitChange();
@@ -1622,11 +2131,28 @@ export async function selectStructureEntry(name) {
1622
2131
  await loadStructureDetail(++routeLoadVersion);
1623
2132
  }
1624
2133
 
2134
+ export function toggleStructureTablesPanel() {
2135
+ state.structure.tablesVisible = state.structure.tablesVisible === false;
2136
+ emitChange();
2137
+ }
2138
+
1625
2139
  export function setTableDesignerSearchQuery(query) {
1626
2140
  state.tableDesigner.searchQuery = String(query ?? "");
1627
2141
  emitChange();
1628
2142
  }
1629
2143
 
2144
+ export function setTableDesignerSqlPreviewVisibility(visible) {
2145
+ const nextValue =
2146
+ typeof visible === "boolean" ? visible : !Boolean(state.tableDesigner.sqlPreviewVisible);
2147
+
2148
+ if (state.tableDesigner.sqlPreviewVisible === nextValue) {
2149
+ return;
2150
+ }
2151
+
2152
+ state.tableDesigner.sqlPreviewVisible = nextValue;
2153
+ emitChange();
2154
+ }
2155
+
1630
2156
  export function updateCurrentTableDesignerField(field, value) {
1631
2157
  if (!state.tableDesigner.draft) {
1632
2158
  return;
@@ -1799,6 +2325,11 @@ export function setDataSearchColumn(columnName) {
1799
2325
  emitChange();
1800
2326
  }
1801
2327
 
2328
+ export function toggleDataTablesPanel() {
2329
+ state.dataBrowser.tablesVisible = state.dataBrowser.tablesVisible === false;
2330
+ emitChange();
2331
+ }
2332
+
1802
2333
  export async function setDataPage(page) {
1803
2334
  const numericPage = Number(page);
1804
2335
 
@@ -2061,6 +2592,116 @@ export async function submitDeleteRowConfirmation() {
2061
2592
  return result;
2062
2593
  }
2063
2594
 
2595
+ export async function saveCurrentQueryChartDraft() {
2596
+ if (state.modal?.kind !== "chart-editor" || !state.modal.draft) {
2597
+ return null;
2598
+ }
2599
+
2600
+ const analysis = getChartsResultAnalysis();
2601
+
2602
+ if (!analysis) {
2603
+ state.modal.error = {
2604
+ code: "RESULT_SET_REQUIRED",
2605
+ message: "Reload the query result before saving a chart.",
2606
+ };
2607
+ emitChange();
2608
+ return null;
2609
+ }
2610
+
2611
+ const validation = validateQueryChartConfig(
2612
+ state.modal.draft.chartType,
2613
+ state.modal.draft.config,
2614
+ analysis
2615
+ );
2616
+
2617
+ if (!validation.valid) {
2618
+ state.modal.error = {
2619
+ code: "VALIDATION_ERROR",
2620
+ message: validation.errors.join(" "),
2621
+ };
2622
+ emitChange();
2623
+ return null;
2624
+ }
2625
+
2626
+ startModalSubmission();
2627
+ const draftMode = state.modal.draft.mode;
2628
+
2629
+ const payload = {
2630
+ queryHistoryId: state.modal.draft.queryHistoryId,
2631
+ name: state.modal.draft.name,
2632
+ chartType: state.modal.draft.chartType,
2633
+ config: state.modal.draft.config,
2634
+ resultColumns: buildQueryChartResultColumns(analysis),
2635
+ tableVisible: state.modal.draft.tableVisible,
2636
+ };
2637
+
2638
+ try {
2639
+ const response =
2640
+ state.modal.draft.mode === "edit"
2641
+ ? await api.updateQueryHistoryChart(state.modal.draft.chartId, payload)
2642
+ : await api.createQueryHistoryChart(payload);
2643
+ const chart = response.data ?? null;
2644
+
2645
+ if (chart) {
2646
+ upsertChartDetailItem(chart);
2647
+ closeModalInternal();
2648
+ pushToast(response.message || (draftMode === "edit" ? "Chart updated." : "Chart created."), "success");
2649
+ return chart;
2650
+ }
2651
+
2652
+ closeModalInternal();
2653
+ return null;
2654
+ } catch (error) {
2655
+ withModalError(error);
2656
+ return null;
2657
+ }
2658
+ }
2659
+
2660
+ export async function toggleQueryChartTableVisibility(chartId) {
2661
+ const chart = state.charts.detail?.charts?.find((entry) => entry.id === Number(chartId)) ?? null;
2662
+
2663
+ if (!chart) {
2664
+ pushToast("The selected chart could not be loaded.", "alert");
2665
+ return null;
2666
+ }
2667
+
2668
+ try {
2669
+ const response = await api.updateQueryHistoryChart(chart.id, {
2670
+ tableVisible: !chart.tableVisible,
2671
+ });
2672
+
2673
+ upsertChartDetailItem(response.data ?? null);
2674
+ emitChange();
2675
+ return response.data ?? null;
2676
+ } catch (error) {
2677
+ pushToast(normalizeError(error)?.message || "Chart update failed.", "alert");
2678
+ return null;
2679
+ }
2680
+ }
2681
+
2682
+ export async function deleteQueryChart(chartId) {
2683
+ try {
2684
+ const response = await api.deleteQueryHistoryChart(chartId);
2685
+ removeChartDetailItem(chartId);
2686
+ closeModalInternal();
2687
+ pushToast(response.message || "Chart deleted.", "muted");
2688
+ emitChange();
2689
+ return true;
2690
+ } catch (error) {
2691
+ withModalError(error);
2692
+ return false;
2693
+ }
2694
+ }
2695
+
2696
+ export async function submitDeleteChartConfirmation() {
2697
+ if (state.modal?.kind !== "delete-chart") {
2698
+ return false;
2699
+ }
2700
+
2701
+ startModalSubmission();
2702
+ return deleteQueryChart(state.modal.chartId);
2703
+ }
2704
+
2064
2705
  export async function exportCurrentQueryCsv() {
2065
2706
  state.editor.exportLoading = true;
2066
2707
  emitChange();