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.
- package/README.md +107 -0
- package/bin/sqlite-hub.js +285 -4
- package/changelog.md +21 -0
- package/docs/DESIGN_GUIDELINES.md +36 -0
- package/frontend/assets/mockups/sql_editor_croped.png +0 -0
- package/frontend/index.html +80 -0
- package/frontend/js/api.js +34 -0
- package/frontend/js/app.js +188 -10
- package/frontend/js/components/connectionCard.js +3 -4
- package/frontend/js/components/emptyState.js +4 -4
- package/frontend/js/components/modal.js +341 -24
- package/frontend/js/components/queryChartRenderer.js +125 -0
- package/frontend/js/components/queryEditor.js +33 -6
- package/frontend/js/components/queryHistoryDetail.js +16 -7
- package/frontend/js/components/queryHistoryPanel.js +18 -7
- package/frontend/js/components/rowEditorPanel.js +59 -54
- package/frontend/js/components/sidebar.js +32 -32
- package/frontend/js/components/structureGraph.js +54 -122
- package/frontend/js/components/tableDesignerEditor.js +9 -8
- package/frontend/js/components/tableDesignerSidebar.js +52 -48
- package/frontend/js/components/tableDesignerSqlPreview.js +60 -28
- package/frontend/js/lib/queryChartOptions.js +283 -0
- package/frontend/js/lib/queryCharts.js +560 -0
- package/frontend/js/router.js +8 -0
- package/frontend/js/store.js +641 -0
- package/frontend/js/views/charts.js +499 -0
- package/frontend/js/views/connections.js +5 -17
- package/frontend/js/views/data.js +40 -27
- package/frontend/js/views/editor.js +19 -13
- package/frontend/js/views/overview.js +149 -10
- package/frontend/js/views/settings.js +2 -2
- package/frontend/js/views/structure.js +105 -59
- package/frontend/js/views/tableDesigner.js +7 -2
- package/frontend/styles/base.css +62 -0
- package/frontend/styles/components.css +68 -118
- package/frontend/styles/structure-graph.css +19 -82
- package/frontend/styles/tokens.css +2 -0
- package/frontend/styles/views.css +293 -66
- package/package.json +2 -1
- package/server/routes/charts.js +153 -0
- package/server/server.js +6 -0
- package/server/services/sqlite/overviewService.js +68 -0
- package/server/services/storage/appStateStore.js +400 -1
- package/server/services/storage/queryHistoryChartUtils.js +145 -0
package/frontend/js/store.js
CHANGED
|
@@ -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();
|