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
@@ -1,5 +1,23 @@
1
1
  import * as api from "./api.js";
2
2
  import { formatCellValue, inferStatusTone, truncateMiddle } from "./utils/format.js";
3
+ import {
4
+ addTableDesignerColumn,
5
+ createTableDesignerDraftFromCsvImport,
6
+ createNewTableDesignerDraft,
7
+ hydrateTableDesignerDraft,
8
+ removeTableDesignerColumn,
9
+ updateTableDesignerColumnField,
10
+ updateTableDesignerDraftField,
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";
3
21
 
4
22
  const listeners = new Set();
5
23
  const DEFAULT_SETTINGS = {
@@ -10,6 +28,7 @@ const DEFAULT_SETTINGS = {
10
28
  const DATA_PAGE_SIZES = [25, 50, 100];
11
29
  const QUERY_HISTORY_PAGE_SIZE = 30;
12
30
  const QUERY_HISTORY_RUN_LIMIT = 8;
31
+ const CHART_HEIGHT_PRESETS = new Set(["small", "medium", "large"]);
13
32
  const MISSING_DATABASE_ERROR = {
14
33
  code: "ACTIVE_DATABASE_REQUIRED",
15
34
  message: "No active SQLite database selected.",
@@ -19,6 +38,8 @@ let routeLoadVersion = 0;
19
38
  let queryHistoryLoadVersion = 0;
20
39
  let queryHistoryDetailLoadVersion = 0;
21
40
  let queryHistorySearchTimer = null;
41
+ let chartsLoadVersion = 0;
42
+ let chartsDetailLoadVersion = 0;
22
43
 
23
44
  const state = {
24
45
  ready: false,
@@ -46,6 +67,7 @@ const state = {
46
67
  dataBrowser: {
47
68
  tables: [],
48
69
  selectedTable: null,
70
+ tablesVisible: true,
49
71
  table: null,
50
72
  loading: false,
51
73
  tableLoading: false,
@@ -64,7 +86,9 @@ const state = {
64
86
  },
65
87
  editor: {
66
88
  sqlText: "",
89
+ editorPanelVisible: true,
67
90
  history: [],
91
+ historyPanelVisible: true,
68
92
  historyLoading: false,
69
93
  historyLoadingMore: false,
70
94
  historyError: null,
@@ -92,6 +116,34 @@ const state = {
92
116
  deleting: false,
93
117
  saveError: null,
94
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
+ },
134
+ tableDesigner: {
135
+ tables: [],
136
+ selectedTableName: null,
137
+ draft: null,
138
+ pendingImportedDraft: null,
139
+ loading: false,
140
+ detailLoading: false,
141
+ saving: false,
142
+ searchQuery: "",
143
+ supportedTypes: [],
144
+ error: null,
145
+ saveError: null,
146
+ },
95
147
  structure: {
96
148
  data: null,
97
149
  selectedName: null,
@@ -145,7 +197,15 @@ function clearQueryHistoryDetailState() {
145
197
  }
146
198
 
147
199
  function requiresActiveDatabase(routeName) {
148
- return ["overview", "data", "editor", "editorResults", "structure"].includes(routeName);
200
+ return [
201
+ "overview",
202
+ "data",
203
+ "editor",
204
+ "editorResults",
205
+ "charts",
206
+ "structure",
207
+ "tableDesigner",
208
+ ].includes(routeName);
149
209
  }
150
210
 
151
211
  function normalizeDataPageSize(value, fallback = 50) {
@@ -270,6 +330,22 @@ function getCurrentStructureEntry(snapshot = state) {
270
330
  return entries.find((entry) => entry.name === snapshot.structure.selectedName) ?? null;
271
331
  }
272
332
 
333
+ function getTableDesignerContext(snapshot = state) {
334
+ return {
335
+ catalogTables: snapshot.tableDesigner.tables ?? [],
336
+ supportedTypes: snapshot.tableDesigner.supportedTypes ?? [],
337
+ readOnly: Boolean(snapshot.connections.active?.readOnly),
338
+ };
339
+ }
340
+
341
+ function decorateTableDesignerDraft(draft, snapshot = state) {
342
+ if (!draft) {
343
+ return null;
344
+ }
345
+
346
+ return hydrateTableDesignerDraft(draft, getTableDesignerContext(snapshot));
347
+ }
348
+
273
349
  function buildDeleteRowPreview(fields = []) {
274
350
  return fields
275
351
  .filter((field) => field && field.label)
@@ -330,6 +406,52 @@ function resetQueryHistoryState({ preserveSearch = true } = {}) {
330
406
  }
331
407
  }
332
408
 
409
+ function resetChartsState() {
410
+ chartsLoadVersion += 1;
411
+ chartsDetailLoadVersion += 1;
412
+ state.charts.queries = [];
413
+ state.charts.loading = false;
414
+ state.charts.error = null;
415
+ state.charts.selectedHistoryId = null;
416
+ state.charts.chartHeightPreset = "medium";
417
+ state.charts.sqlExpanded = false;
418
+ state.charts.resultsVisible = true;
419
+ state.charts.detail = null;
420
+ state.charts.detailLoading = false;
421
+ state.charts.detailError = null;
422
+ state.charts.result = null;
423
+ state.charts.resultLoading = false;
424
+ state.charts.resultError = null;
425
+ }
426
+
427
+ function normalizeChartsHeightPreset(value) {
428
+ const normalizedValue = String(value ?? "")
429
+ .trim()
430
+ .toLowerCase();
431
+
432
+ return CHART_HEIGHT_PRESETS.has(normalizedValue) ? normalizedValue : "medium";
433
+ }
434
+
435
+ function mergeQueryHistoryItemWithChartSummary(updatedItem, fallbackItem = null) {
436
+ if (!updatedItem) {
437
+ return updatedItem;
438
+ }
439
+
440
+ return {
441
+ ...(fallbackItem ?? {}),
442
+ ...updatedItem,
443
+ chartCount:
444
+ updatedItem.chartCount !== undefined
445
+ ? Number(updatedItem.chartCount ?? 0)
446
+ : Number(fallbackItem?.chartCount ?? 0),
447
+ chartTypes: Array.isArray(updatedItem.chartTypes)
448
+ ? [...updatedItem.chartTypes]
449
+ : Array.isArray(fallbackItem?.chartTypes)
450
+ ? [...fallbackItem.chartTypes]
451
+ : [],
452
+ };
453
+ }
454
+
333
455
  function syncQueryHistoryItem(updatedItem) {
334
456
  if (!updatedItem) {
335
457
  return;
@@ -342,6 +464,61 @@ function syncQueryHistoryItem(updatedItem) {
342
464
  if (state.editor.historyDetail?.id === updatedItem.id) {
343
465
  state.editor.historyDetail = updatedItem;
344
466
  }
467
+
468
+ const existingChartsEntry =
469
+ state.charts.queries.find((entry) => entry.id === updatedItem.id) ??
470
+ (state.charts.detail?.item?.id === updatedItem.id ? state.charts.detail.item : null);
471
+ const mergedChartsItem = mergeQueryHistoryItemWithChartSummary(updatedItem, existingChartsEntry);
472
+
473
+ state.charts.queries = state.charts.queries.map((entry) =>
474
+ entry.id === updatedItem.id ? mergedChartsItem : entry
475
+ );
476
+
477
+ if (state.charts.detail?.item?.id === updatedItem.id) {
478
+ state.charts.detail = {
479
+ ...state.charts.detail,
480
+ item: mergeQueryHistoryItemWithChartSummary(updatedItem, state.charts.detail.item),
481
+ };
482
+ }
483
+ }
484
+
485
+ function syncChartsQuerySummaryForHistory(historyId) {
486
+ const numericId = Number(historyId);
487
+
488
+ if (!Number.isInteger(numericId) || numericId < 1 || state.charts.detail?.item?.id !== numericId) {
489
+ return;
490
+ }
491
+
492
+ const charts = state.charts.detail?.charts ?? [];
493
+ const chartTypes = [...new Set(
494
+ charts
495
+ .map((chart) =>
496
+ String(chart?.chartType ?? "")
497
+ .trim()
498
+ .toLowerCase()
499
+ )
500
+ .filter(Boolean)
501
+ )];
502
+ const chartCount = charts.length;
503
+
504
+ state.charts.queries = state.charts.queries.map((entry) =>
505
+ entry.id === numericId
506
+ ? {
507
+ ...entry,
508
+ chartCount,
509
+ chartTypes,
510
+ }
511
+ : entry
512
+ );
513
+
514
+ state.charts.detail = {
515
+ ...state.charts.detail,
516
+ item: {
517
+ ...state.charts.detail.item,
518
+ chartCount,
519
+ chartTypes,
520
+ },
521
+ };
345
522
  }
346
523
 
347
524
  function resolveQueryHistorySql(historyId) {
@@ -351,6 +528,10 @@ function resolveQueryHistorySql(historyId) {
351
528
  return state.editor.historyDetail.rawSql;
352
529
  }
353
530
 
531
+ if (String(state.charts.detail?.item?.id ?? "") === historyIdAsString) {
532
+ return state.charts.detail.item.rawSql;
533
+ }
534
+
354
535
  return findQueryHistoryItem(historyId)?.rawSql ?? null;
355
536
  }
356
537
 
@@ -358,6 +539,11 @@ function clearRouteSlices() {
358
539
  state.overview.error = null;
359
540
  state.dataBrowser.error = null;
360
541
  state.dataBrowser.saveError = null;
542
+ state.charts.error = null;
543
+ state.charts.detailError = null;
544
+ state.charts.resultError = null;
545
+ state.tableDesigner.error = null;
546
+ state.tableDesigner.saveError = null;
361
547
  state.structure.error = null;
362
548
  }
363
549
 
@@ -387,6 +573,21 @@ function setMissingDatabaseState() {
387
573
  state.structure.detail = null;
388
574
  state.structure.error = error;
389
575
 
576
+ state.tableDesigner.loading = false;
577
+ state.tableDesigner.detailLoading = false;
578
+ state.tableDesigner.tables = [];
579
+ state.tableDesigner.selectedTableName = null;
580
+ state.tableDesigner.draft = null;
581
+ state.tableDesigner.pendingImportedDraft = null;
582
+ state.tableDesigner.saving = false;
583
+ state.tableDesigner.searchQuery = "";
584
+ state.tableDesigner.supportedTypes = [];
585
+ state.tableDesigner.error = error;
586
+ state.tableDesigner.saveError = null;
587
+
588
+ resetChartsState();
589
+ state.charts.error = error;
590
+
390
591
  resetQueryHistoryState({ preserveSearch: false });
391
592
  }
392
593
 
@@ -423,6 +624,10 @@ function syncRouteContext() {
423
624
  state.structure.detail = null;
424
625
  state.structure.selectedName = null;
425
626
  }
627
+
628
+ if (route.name !== "tableDesigner") {
629
+ state.tableDesigner.saveError = null;
630
+ }
426
631
  }
427
632
 
428
633
  async function refreshConnectionsState() {
@@ -595,6 +800,114 @@ async function refreshQueryHistoryState({ append = false } = {}) {
595
800
  }
596
801
  }
597
802
 
803
+ async function loadChartsDetail(historyId) {
804
+ const numericId = Number(historyId);
805
+ const requestVersion = ++chartsDetailLoadVersion;
806
+
807
+ if (!Number.isInteger(numericId) || numericId < 1) {
808
+ state.charts.selectedHistoryId = null;
809
+ state.charts.sqlExpanded = false;
810
+ state.charts.resultsVisible = true;
811
+ state.charts.detail = null;
812
+ state.charts.detailLoading = false;
813
+ state.charts.detailError = null;
814
+ state.charts.result = null;
815
+ state.charts.resultLoading = false;
816
+ state.charts.resultError = null;
817
+ emitChange();
818
+ return;
819
+ }
820
+
821
+ state.charts.selectedHistoryId = numericId;
822
+ state.charts.sqlExpanded = false;
823
+ state.charts.resultsVisible = true;
824
+ state.charts.detail = null;
825
+ state.charts.detailLoading = true;
826
+ state.charts.detailError = null;
827
+ state.charts.result = null;
828
+ state.charts.resultLoading = true;
829
+ state.charts.resultError = null;
830
+ emitChange();
831
+
832
+ const [detailResponse, resultResponse] = await Promise.allSettled([
833
+ api.getChartsQueryHistoryDetail(numericId),
834
+ api.executeChartsQueryHistory(numericId),
835
+ ]);
836
+
837
+ if (
838
+ requestVersion !== chartsDetailLoadVersion ||
839
+ state.charts.selectedHistoryId !== numericId
840
+ ) {
841
+ return;
842
+ }
843
+
844
+ if (detailResponse.status === "fulfilled") {
845
+ state.charts.detail = detailResponse.value.data ?? null;
846
+ state.charts.detailError = null;
847
+ if (detailResponse.value.data?.item) {
848
+ syncQueryHistoryItem(detailResponse.value.data.item);
849
+ syncChartsQuerySummaryForHistory(detailResponse.value.data.item.id);
850
+ }
851
+ } else {
852
+ state.charts.detail = null;
853
+ state.charts.detailError = normalizeError(detailResponse.reason);
854
+ }
855
+
856
+ if (resultResponse.status === "fulfilled") {
857
+ state.charts.result = resultResponse.value.data ?? null;
858
+ state.charts.resultError = null;
859
+ } else {
860
+ state.charts.result = null;
861
+ state.charts.resultError = normalizeError(resultResponse.reason);
862
+ }
863
+
864
+ if (requestVersion === chartsDetailLoadVersion) {
865
+ state.charts.detailLoading = false;
866
+ state.charts.resultLoading = false;
867
+ emitChange();
868
+ }
869
+ }
870
+
871
+ async function loadCharts(version, route) {
872
+ state.charts.loading = true;
873
+ state.charts.error = null;
874
+ emitChange();
875
+
876
+ try {
877
+ const response = await api.getChartsQueryHistory();
878
+
879
+ if (version !== routeLoadVersion) {
880
+ return;
881
+ }
882
+
883
+ state.charts.queries = response.data ?? [];
884
+ state.charts.error = null;
885
+ } catch (error) {
886
+ if (version !== routeLoadVersion) {
887
+ return;
888
+ }
889
+
890
+ state.charts.queries = [];
891
+ state.charts.error = normalizeError(error);
892
+ } finally {
893
+ if (version === routeLoadVersion) {
894
+ state.charts.loading = false;
895
+ emitChange();
896
+ }
897
+ }
898
+
899
+ if (version !== routeLoadVersion) {
900
+ return;
901
+ }
902
+
903
+ const requestedHistoryId = Number(route.params?.historyId ?? null);
904
+ const canLoadRequestedHistory =
905
+ Number.isInteger(requestedHistoryId) &&
906
+ state.charts.queries.some((item) => item.id === requestedHistoryId);
907
+
908
+ await loadChartsDetail(canLoadRequestedHistory ? requestedHistoryId : null);
909
+ }
910
+
598
911
  async function loadOverview(version) {
599
912
  state.overview.loading = true;
600
913
  state.overview.error = null;
@@ -824,6 +1137,103 @@ async function loadStructure(version) {
824
1137
  }
825
1138
  }
826
1139
 
1140
+ async function loadTableDesignerDetail(version, tableName) {
1141
+ if (!tableName) {
1142
+ state.tableDesigner.draft = null;
1143
+ return;
1144
+ }
1145
+
1146
+ state.tableDesigner.detailLoading = true;
1147
+ state.tableDesigner.saveError = null;
1148
+ emitChange();
1149
+
1150
+ try {
1151
+ const response = await api.getTableDesignerTable(tableName);
1152
+
1153
+ if (version !== routeLoadVersion) {
1154
+ return;
1155
+ }
1156
+
1157
+ state.tableDesigner.selectedTableName = tableName;
1158
+ state.tableDesigner.draft = decorateTableDesignerDraft(response.data?.draft ?? null);
1159
+ } catch (error) {
1160
+ if (version !== routeLoadVersion) {
1161
+ return;
1162
+ }
1163
+
1164
+ state.tableDesigner.error = normalizeError(error);
1165
+ state.tableDesigner.draft = null;
1166
+ } finally {
1167
+ if (version === routeLoadVersion) {
1168
+ state.tableDesigner.detailLoading = false;
1169
+ emitChange();
1170
+ }
1171
+ }
1172
+ }
1173
+
1174
+ async function loadTableDesigner(version, route) {
1175
+ state.tableDesigner.loading = true;
1176
+ state.tableDesigner.error = null;
1177
+ emitChange();
1178
+
1179
+ try {
1180
+ const response = await api.getTableDesignerOverview();
1181
+
1182
+ if (version !== routeLoadVersion) {
1183
+ return;
1184
+ }
1185
+
1186
+ state.tableDesigner.tables = response.data?.tables ?? [];
1187
+ state.tableDesigner.supportedTypes = response.data?.supportedTypes ?? [];
1188
+
1189
+ if (route.params?.isNew) {
1190
+ const importedDraft = state.tableDesigner.pendingImportedDraft;
1191
+ state.tableDesigner.selectedTableName = null;
1192
+ state.tableDesigner.detailLoading = false;
1193
+ state.tableDesigner.pendingImportedDraft = null;
1194
+ state.tableDesigner.draft = decorateTableDesignerDraft(
1195
+ importedDraft ?? createNewTableDesignerDraft()
1196
+ );
1197
+ return;
1198
+ }
1199
+
1200
+ const requestedTableName = route.params?.tableName ?? null;
1201
+ const tableName =
1202
+ requestedTableName &&
1203
+ state.tableDesigner.tables.some((table) => table.name === requestedTableName)
1204
+ ? requestedTableName
1205
+ : state.tableDesigner.selectedTableName &&
1206
+ state.tableDesigner.tables.some(
1207
+ (table) => table.name === state.tableDesigner.selectedTableName
1208
+ )
1209
+ ? state.tableDesigner.selectedTableName
1210
+ : state.tableDesigner.tables[0]?.name ?? null;
1211
+
1212
+ if (!tableName) {
1213
+ state.tableDesigner.selectedTableName = null;
1214
+ state.tableDesigner.detailLoading = false;
1215
+ state.tableDesigner.draft = null;
1216
+ return;
1217
+ }
1218
+
1219
+ await loadTableDesignerDetail(version, tableName);
1220
+ } catch (error) {
1221
+ if (version !== routeLoadVersion) {
1222
+ return;
1223
+ }
1224
+
1225
+ state.tableDesigner.tables = [];
1226
+ state.tableDesigner.selectedTableName = null;
1227
+ state.tableDesigner.draft = null;
1228
+ state.tableDesigner.error = normalizeError(error);
1229
+ } finally {
1230
+ if (version === routeLoadVersion) {
1231
+ state.tableDesigner.loading = false;
1232
+ emitChange();
1233
+ }
1234
+ }
1235
+ }
1236
+
827
1237
  function invalidateDatabaseCaches() {
828
1238
  state.overview.data = null;
829
1239
  state.dataBrowser.tables = [];
@@ -835,6 +1245,16 @@ function invalidateDatabaseCaches() {
835
1245
  state.dataBrowser.exportLoading = false;
836
1246
  state.dataBrowser.error = null;
837
1247
  state.dataBrowser.saveError = null;
1248
+ state.tableDesigner.tables = [];
1249
+ state.tableDesigner.selectedTableName = null;
1250
+ state.tableDesigner.draft = null;
1251
+ state.tableDesigner.pendingImportedDraft = null;
1252
+ state.tableDesigner.saving = false;
1253
+ state.tableDesigner.searchQuery = "";
1254
+ state.tableDesigner.supportedTypes = [];
1255
+ state.tableDesigner.error = null;
1256
+ state.tableDesigner.saveError = null;
1257
+ resetChartsState();
838
1258
  state.structure.data = null;
839
1259
  state.structure.detail = null;
840
1260
  }
@@ -862,6 +1282,9 @@ async function loadRouteData(route) {
862
1282
  case "data":
863
1283
  await loadData(version, route);
864
1284
  return;
1285
+ case "charts":
1286
+ await loadCharts(version, route);
1287
+ return;
865
1288
  case "editor":
866
1289
  case "editorResults":
867
1290
  await refreshQueryHistoryState();
@@ -869,6 +1292,9 @@ async function loadRouteData(route) {
869
1292
  case "structure":
870
1293
  await loadStructure(version);
871
1294
  return;
1295
+ case "tableDesigner":
1296
+ await loadTableDesigner(version, route);
1297
+ return;
872
1298
  case "settings":
873
1299
  await refreshSettingsState();
874
1300
  return;
@@ -911,6 +1337,94 @@ function closeModalInternal() {
911
1337
  emitChange();
912
1338
  }
913
1339
 
1340
+ function getChartsResultAnalysis(snapshot = state) {
1341
+ return snapshot.charts.result ? analyzeQueryChartResult(snapshot.charts.result) : null;
1342
+ }
1343
+
1344
+ function buildQueryChartDraft(mode, chart = null) {
1345
+ const queryItem = state.charts.detail?.item ?? null;
1346
+ const analysis = getChartsResultAnalysis();
1347
+
1348
+ if (!queryItem || !analysis) {
1349
+ return null;
1350
+ }
1351
+
1352
+ const existingCharts = state.charts.detail?.charts ?? [];
1353
+
1354
+ if (mode === "edit" && chart) {
1355
+ return {
1356
+ mode,
1357
+ chartId: chart.id,
1358
+ queryHistoryId: chart.queryHistoryId,
1359
+ chartType: chart.chartType,
1360
+ name: chart.name,
1361
+ nameTouched: true,
1362
+ config: structuredClone(chart.config),
1363
+ tableVisible: chart.tableVisible,
1364
+ resultColumns: buildQueryChartResultColumns(analysis),
1365
+ };
1366
+ }
1367
+
1368
+ const chartType = suggestQueryChartType(analysis);
1369
+ const name = resolveUniqueQueryChartName(
1370
+ buildDefaultQueryChartName(chartType, queryItem.displayTitle),
1371
+ existingCharts
1372
+ );
1373
+
1374
+ return {
1375
+ mode: "create",
1376
+ chartId: null,
1377
+ queryHistoryId: queryItem.id,
1378
+ chartType,
1379
+ name,
1380
+ nameTouched: false,
1381
+ config: buildSuggestedChartConfig(chartType, analysis),
1382
+ tableVisible: true,
1383
+ resultColumns: buildQueryChartResultColumns(analysis),
1384
+ };
1385
+ }
1386
+
1387
+ function upsertChartDetailItem(updatedChart) {
1388
+ if (!updatedChart || state.charts.detail?.item?.id !== updatedChart.queryHistoryId) {
1389
+ return;
1390
+ }
1391
+
1392
+ const nextCharts = [...(state.charts.detail?.charts ?? [])];
1393
+ const existingIndex = nextCharts.findIndex((chart) => chart.id === updatedChart.id);
1394
+
1395
+ if (existingIndex >= 0) {
1396
+ nextCharts.splice(existingIndex, 1, updatedChart);
1397
+ } else {
1398
+ nextCharts.push(updatedChart);
1399
+ }
1400
+
1401
+ nextCharts.sort((left, right) => {
1402
+ const leftTime = Date.parse(left.createdAt ?? "") || 0;
1403
+ const rightTime = Date.parse(right.createdAt ?? "") || 0;
1404
+ return leftTime - rightTime || left.id - right.id;
1405
+ });
1406
+
1407
+ state.charts.detail = {
1408
+ ...state.charts.detail,
1409
+ charts: nextCharts,
1410
+ };
1411
+
1412
+ syncChartsQuerySummaryForHistory(updatedChart.queryHistoryId);
1413
+ }
1414
+
1415
+ function removeChartDetailItem(chartId) {
1416
+ if (!state.charts.detail) {
1417
+ return;
1418
+ }
1419
+
1420
+ state.charts.detail = {
1421
+ ...state.charts.detail,
1422
+ charts: (state.charts.detail.charts ?? []).filter((chart) => chart.id !== Number(chartId)),
1423
+ };
1424
+
1425
+ syncChartsQuerySummaryForHistory(state.charts.detail.item?.id);
1426
+ }
1427
+
914
1428
  export function getState() {
915
1429
  return clone(state);
916
1430
  }
@@ -1033,10 +1547,147 @@ export function openDeleteEditorRowModal(rowIndex) {
1033
1547
  emitChange();
1034
1548
  }
1035
1549
 
1550
+ export function openCreateQueryChartModal() {
1551
+ const draft = buildQueryChartDraft("create");
1552
+
1553
+ if (!state.charts.detail?.item || !state.charts.result) {
1554
+ pushToast("The selected query has no chartable result set yet.", "alert");
1555
+ return;
1556
+ }
1557
+
1558
+ if (!draft) {
1559
+ pushToast("The chart editor could not be opened for this query.", "alert");
1560
+ return;
1561
+ }
1562
+
1563
+ state.modal = {
1564
+ kind: "chart-editor",
1565
+ error: null,
1566
+ submitting: false,
1567
+ draft,
1568
+ };
1569
+ emitChange();
1570
+ }
1571
+
1572
+ export function openEditQueryChartModal(chartId) {
1573
+ if (!state.charts.result) {
1574
+ pushToast("Reload the query result before editing this chart.", "alert");
1575
+ return;
1576
+ }
1577
+
1578
+ const chart = state.charts.detail?.charts?.find((entry) => entry.id === Number(chartId)) ?? null;
1579
+ const draft = chart ? buildQueryChartDraft("edit", chart) : null;
1580
+
1581
+ if (!chart || !draft) {
1582
+ pushToast("The selected chart could not be loaded.", "alert");
1583
+ return;
1584
+ }
1585
+
1586
+ state.modal = {
1587
+ kind: "chart-editor",
1588
+ error: null,
1589
+ submitting: false,
1590
+ draft,
1591
+ };
1592
+ emitChange();
1593
+ }
1594
+
1595
+ export function openDeleteQueryChartModal(chartId) {
1596
+ const chart = state.charts.detail?.charts?.find((entry) => entry.id === Number(chartId)) ?? null;
1597
+
1598
+ if (!chart) {
1599
+ pushToast("The selected chart could not be loaded.", "alert");
1600
+ return;
1601
+ }
1602
+
1603
+ state.modal = {
1604
+ kind: "delete-chart",
1605
+ chartId: chart.id,
1606
+ chartName: chart.name,
1607
+ error: null,
1608
+ submitting: false,
1609
+ };
1610
+ emitChange();
1611
+ }
1612
+
1036
1613
  export function closeModal() {
1037
1614
  closeModalInternal();
1038
1615
  }
1039
1616
 
1617
+ export function toggleChartsSqlPanel() {
1618
+ state.charts.sqlExpanded = !state.charts.sqlExpanded;
1619
+ emitChange();
1620
+ }
1621
+
1622
+ export function toggleChartsResultsPanel() {
1623
+ state.charts.resultsVisible = !state.charts.resultsVisible;
1624
+ emitChange();
1625
+ }
1626
+
1627
+ export function setChartsHeightPreset(preset) {
1628
+ const nextPreset = normalizeChartsHeightPreset(preset);
1629
+
1630
+ if (state.charts.chartHeightPreset === nextPreset) {
1631
+ return;
1632
+ }
1633
+
1634
+ state.charts.chartHeightPreset = nextPreset;
1635
+ emitChange();
1636
+ }
1637
+
1638
+ export function updateCurrentQueryChartDraftField(field, value) {
1639
+ if (state.modal?.kind !== "chart-editor" || !state.modal.draft) {
1640
+ return;
1641
+ }
1642
+
1643
+ state.modal.error = null;
1644
+
1645
+ if (field === "name") {
1646
+ state.modal.draft.name = String(value ?? "");
1647
+ state.modal.draft.nameTouched = true;
1648
+ emitChange();
1649
+ return;
1650
+ }
1651
+
1652
+ if (field === "chartType") {
1653
+ const nextType = String(value ?? "").trim().toLowerCase();
1654
+ const analysis = getChartsResultAnalysis();
1655
+
1656
+ state.modal.draft.chartType = nextType;
1657
+ state.modal.draft.config = buildSuggestedChartConfig(nextType, analysis);
1658
+
1659
+ if (!state.modal.draft.nameTouched) {
1660
+ const existingCharts = state.charts.detail?.charts ?? [];
1661
+ state.modal.draft.name = resolveUniqueQueryChartName(
1662
+ buildDefaultQueryChartName(nextType, state.charts.detail?.item?.displayTitle),
1663
+ existingCharts,
1664
+ state.modal.draft.chartId
1665
+ );
1666
+ }
1667
+
1668
+ emitChange();
1669
+ return;
1670
+ }
1671
+
1672
+ if (field === "tableVisible") {
1673
+ state.modal.draft.tableVisible = Boolean(value);
1674
+ emitChange();
1675
+ }
1676
+ }
1677
+
1678
+ export function updateCurrentQueryChartDraftConfigField(field, value) {
1679
+ if (state.modal?.kind !== "chart-editor" || !state.modal.draft) {
1680
+ return;
1681
+ }
1682
+
1683
+ state.modal.error = null;
1684
+ state.modal.draft.config = {
1685
+ ...state.modal.draft.config,
1686
+ [field]: value,
1687
+ };
1688
+ emitChange();
1689
+ }
1690
+
1040
1691
  export function dismissToast(id) {
1041
1692
  const nextToasts = state.toasts.filter((toast) => toast.id !== id);
1042
1693
 
@@ -1236,6 +1887,18 @@ export function clearEditorResults() {
1236
1887
  emitChange();
1237
1888
  }
1238
1889
 
1890
+ export function setEditorPanelVisibility(visible) {
1891
+ const nextValue =
1892
+ typeof visible === "boolean" ? visible : !Boolean(state.editor.editorPanelVisible);
1893
+
1894
+ if (state.editor.editorPanelVisible === nextValue) {
1895
+ return;
1896
+ }
1897
+
1898
+ state.editor.editorPanelVisible = nextValue;
1899
+ emitChange();
1900
+ }
1901
+
1239
1902
  export function setEditorTab(tab) {
1240
1903
  state.editor.activeTab = tab;
1241
1904
  emitChange();
@@ -1306,6 +1969,18 @@ export async function setQueryHistoryTab(tab) {
1306
1969
  await refreshQueryHistoryState();
1307
1970
  }
1308
1971
 
1972
+ export function setQueryHistoryPanelVisibility(visible) {
1973
+ const nextValue =
1974
+ typeof visible === "boolean" ? visible : !Boolean(state.editor.historyPanelVisible);
1975
+
1976
+ if (state.editor.historyPanelVisible === nextValue) {
1977
+ return;
1978
+ }
1979
+
1980
+ state.editor.historyPanelVisible = nextValue;
1981
+ emitChange();
1982
+ }
1983
+
1309
1984
  export function setQueryHistorySearchInput(query) {
1310
1985
  state.editor.historySearchInput = String(query ?? "");
1311
1986
  emitChange();
@@ -1450,6 +2125,125 @@ export async function selectStructureEntry(name) {
1450
2125
  await loadStructureDetail(++routeLoadVersion);
1451
2126
  }
1452
2127
 
2128
+ export function setTableDesignerSearchQuery(query) {
2129
+ state.tableDesigner.searchQuery = String(query ?? "");
2130
+ emitChange();
2131
+ }
2132
+
2133
+ export function updateCurrentTableDesignerField(field, value) {
2134
+ if (!state.tableDesigner.draft) {
2135
+ return;
2136
+ }
2137
+
2138
+ state.tableDesigner.draft = updateTableDesignerDraftField(
2139
+ state.tableDesigner.draft,
2140
+ field,
2141
+ value,
2142
+ getTableDesignerContext()
2143
+ );
2144
+ state.tableDesigner.saveError = null;
2145
+ emitChange();
2146
+ }
2147
+
2148
+ export function updateCurrentTableDesignerColumnField(columnId, field, value) {
2149
+ if (!state.tableDesigner.draft) {
2150
+ return;
2151
+ }
2152
+
2153
+ state.tableDesigner.draft = updateTableDesignerColumnField(
2154
+ state.tableDesigner.draft,
2155
+ columnId,
2156
+ field,
2157
+ value,
2158
+ getTableDesignerContext()
2159
+ );
2160
+ state.tableDesigner.saveError = null;
2161
+ emitChange();
2162
+ }
2163
+
2164
+ export function addCurrentTableDesignerColumn() {
2165
+ if (!state.tableDesigner.draft) {
2166
+ return null;
2167
+ }
2168
+
2169
+ const previousColumnIds = new Set(state.tableDesigner.draft.columns.map((column) => column.id));
2170
+ state.tableDesigner.draft = addTableDesignerColumn(
2171
+ state.tableDesigner.draft,
2172
+ getTableDesignerContext()
2173
+ );
2174
+ state.tableDesigner.saveError = null;
2175
+ emitChange();
2176
+
2177
+ const nextColumn = state.tableDesigner.draft.columns.find(
2178
+ (column) => !previousColumnIds.has(column.id)
2179
+ );
2180
+
2181
+ return nextColumn?.id ?? null;
2182
+ }
2183
+
2184
+ export function queueTableDesignerCsvImport(fileName, csvText) {
2185
+ try {
2186
+ const imported = createTableDesignerDraftFromCsvImport(
2187
+ { fileName, csvText },
2188
+ getTableDesignerContext()
2189
+ );
2190
+
2191
+ state.tableDesigner.pendingImportedDraft = imported.draft;
2192
+ state.tableDesigner.selectedTableName = null;
2193
+ state.tableDesigner.saveError = null;
2194
+ state.tableDesigner.error = null;
2195
+ emitChange();
2196
+ return imported;
2197
+ } catch (error) {
2198
+ pushToast(error?.message || "CSV import failed.", "alert");
2199
+ return null;
2200
+ }
2201
+ }
2202
+
2203
+ export function removeCurrentTableDesignerColumn(columnId) {
2204
+ if (!state.tableDesigner.draft) {
2205
+ return;
2206
+ }
2207
+
2208
+ state.tableDesigner.draft = removeTableDesignerColumn(
2209
+ state.tableDesigner.draft,
2210
+ columnId,
2211
+ getTableDesignerContext()
2212
+ );
2213
+ state.tableDesigner.saveError = null;
2214
+ emitChange();
2215
+ }
2216
+
2217
+ export async function saveCurrentTableDesignerDraft() {
2218
+ if (!state.tableDesigner.draft) {
2219
+ return null;
2220
+ }
2221
+
2222
+ state.tableDesigner.saving = true;
2223
+ state.tableDesigner.saveError = null;
2224
+ emitChange();
2225
+
2226
+ try {
2227
+ const response = await api.saveTableDesignerDraft({
2228
+ draft: state.tableDesigner.draft,
2229
+ });
2230
+
2231
+ state.tableDesigner.tables = response.data?.tables ?? state.tableDesigner.tables;
2232
+ state.tableDesigner.selectedTableName =
2233
+ response.data?.savedTableName ?? state.tableDesigner.selectedTableName;
2234
+ state.tableDesigner.draft = decorateTableDesignerDraft(response.data?.draft ?? null);
2235
+ pushToast(response.message || "Table schema saved.", "success");
2236
+ return response.data?.savedTableName ?? null;
2237
+ } catch (error) {
2238
+ state.tableDesigner.saveError = normalizeError(error);
2239
+ emitChange();
2240
+ return null;
2241
+ } finally {
2242
+ state.tableDesigner.saving = false;
2243
+ emitChange();
2244
+ }
2245
+ }
2246
+
1453
2247
  export function selectDataRow(index) {
1454
2248
  const numericIndex = Number(index);
1455
2249
 
@@ -1508,6 +2302,11 @@ export function setDataSearchColumn(columnName) {
1508
2302
  emitChange();
1509
2303
  }
1510
2304
 
2305
+ export function toggleDataTablesPanel() {
2306
+ state.dataBrowser.tablesVisible = state.dataBrowser.tablesVisible === false;
2307
+ emitChange();
2308
+ }
2309
+
1511
2310
  export async function setDataPage(page) {
1512
2311
  const numericPage = Number(page);
1513
2312
 
@@ -1770,6 +2569,116 @@ export async function submitDeleteRowConfirmation() {
1770
2569
  return result;
1771
2570
  }
1772
2571
 
2572
+ export async function saveCurrentQueryChartDraft() {
2573
+ if (state.modal?.kind !== "chart-editor" || !state.modal.draft) {
2574
+ return null;
2575
+ }
2576
+
2577
+ const analysis = getChartsResultAnalysis();
2578
+
2579
+ if (!analysis) {
2580
+ state.modal.error = {
2581
+ code: "RESULT_SET_REQUIRED",
2582
+ message: "Reload the query result before saving a chart.",
2583
+ };
2584
+ emitChange();
2585
+ return null;
2586
+ }
2587
+
2588
+ const validation = validateQueryChartConfig(
2589
+ state.modal.draft.chartType,
2590
+ state.modal.draft.config,
2591
+ analysis
2592
+ );
2593
+
2594
+ if (!validation.valid) {
2595
+ state.modal.error = {
2596
+ code: "VALIDATION_ERROR",
2597
+ message: validation.errors.join(" "),
2598
+ };
2599
+ emitChange();
2600
+ return null;
2601
+ }
2602
+
2603
+ startModalSubmission();
2604
+ const draftMode = state.modal.draft.mode;
2605
+
2606
+ const payload = {
2607
+ queryHistoryId: state.modal.draft.queryHistoryId,
2608
+ name: state.modal.draft.name,
2609
+ chartType: state.modal.draft.chartType,
2610
+ config: state.modal.draft.config,
2611
+ resultColumns: buildQueryChartResultColumns(analysis),
2612
+ tableVisible: state.modal.draft.tableVisible,
2613
+ };
2614
+
2615
+ try {
2616
+ const response =
2617
+ state.modal.draft.mode === "edit"
2618
+ ? await api.updateQueryHistoryChart(state.modal.draft.chartId, payload)
2619
+ : await api.createQueryHistoryChart(payload);
2620
+ const chart = response.data ?? null;
2621
+
2622
+ if (chart) {
2623
+ upsertChartDetailItem(chart);
2624
+ closeModalInternal();
2625
+ pushToast(response.message || (draftMode === "edit" ? "Chart updated." : "Chart created."), "success");
2626
+ return chart;
2627
+ }
2628
+
2629
+ closeModalInternal();
2630
+ return null;
2631
+ } catch (error) {
2632
+ withModalError(error);
2633
+ return null;
2634
+ }
2635
+ }
2636
+
2637
+ export async function toggleQueryChartTableVisibility(chartId) {
2638
+ const chart = state.charts.detail?.charts?.find((entry) => entry.id === Number(chartId)) ?? null;
2639
+
2640
+ if (!chart) {
2641
+ pushToast("The selected chart could not be loaded.", "alert");
2642
+ return null;
2643
+ }
2644
+
2645
+ try {
2646
+ const response = await api.updateQueryHistoryChart(chart.id, {
2647
+ tableVisible: !chart.tableVisible,
2648
+ });
2649
+
2650
+ upsertChartDetailItem(response.data ?? null);
2651
+ emitChange();
2652
+ return response.data ?? null;
2653
+ } catch (error) {
2654
+ pushToast(normalizeError(error)?.message || "Chart update failed.", "alert");
2655
+ return null;
2656
+ }
2657
+ }
2658
+
2659
+ export async function deleteQueryChart(chartId) {
2660
+ try {
2661
+ const response = await api.deleteQueryHistoryChart(chartId);
2662
+ removeChartDetailItem(chartId);
2663
+ closeModalInternal();
2664
+ pushToast(response.message || "Chart deleted.", "muted");
2665
+ emitChange();
2666
+ return true;
2667
+ } catch (error) {
2668
+ withModalError(error);
2669
+ return false;
2670
+ }
2671
+ }
2672
+
2673
+ export async function submitDeleteChartConfirmation() {
2674
+ if (state.modal?.kind !== "delete-chart") {
2675
+ return false;
2676
+ }
2677
+
2678
+ startModalSubmission();
2679
+ return deleteQueryChart(state.modal.chartId);
2680
+ }
2681
+
1773
2682
  export async function exportCurrentQueryCsv() {
1774
2683
  state.editor.exportLoading = true;
1775
2684
  emitChange();
@@ -1845,6 +2754,10 @@ export async function refreshCurrentRoute() {
1845
2754
  await loadRouteData(state.route);
1846
2755
  }
1847
2756
 
2757
+ export function showToast(message, tone = "muted") {
2758
+ pushToast(message, tone);
2759
+ }
2760
+
1848
2761
  export function getCurrentConnection(snapshot = state) {
1849
2762
  return snapshot.connections.active;
1850
2763
  }