sqlite-hub 0.9.1 → 0.9.4
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/.github/workflows/ci.yml +36 -0
- package/README.md +2 -2
- package/bin/sqlite-hub.js +1 -1
- package/frontend/index.html +3 -158
- package/frontend/js/app.js +231 -5
- package/frontend/js/components/connectionCard.js +62 -87
- package/frontend/js/components/emptyState.js +20 -23
- package/frontend/js/components/modal.js +158 -199
- package/frontend/js/components/pageHeader.js +1 -1
- package/frontend/js/components/queryEditor.js +16 -30
- package/frontend/js/components/queryHistoryDetail.js +93 -164
- package/frontend/js/components/queryHistoryPanel.js +90 -100
- package/frontend/js/components/queryResults.js +3 -1
- package/frontend/js/components/rowEditorPanel.js +76 -56
- package/frontend/js/components/tableDesignerEditor.js +91 -116
- package/frontend/js/lib/queryChartOptions.js +5 -1
- package/frontend/js/lib/queryCharts.js +50 -2
- package/frontend/js/store.js +161 -23
- package/frontend/js/utils/tableDesigner.js +8 -2
- package/frontend/js/views/charts.js +126 -73
- package/frontend/js/views/data.js +147 -133
- package/frontend/js/views/editor.js +1 -0
- package/frontend/js/views/mediaTagging.js +131 -164
- package/frontend/js/views/structure.js +52 -48
- package/frontend/styles/base.css +57 -13
- package/frontend/styles/components.css +166 -96
- package/frontend/styles/layout.css +1 -1
- package/frontend/styles/structure-graph.css +11 -11
- package/frontend/styles/tailwind.css +81 -0
- package/frontend/styles/tailwind.generated.css +1 -0
- package/frontend/styles/tokens.css +50 -7
- package/frontend/styles/utilities.css +21 -0
- package/frontend/styles/views.css +94 -86
- package/package.json +16 -3
- package/server/routes/mediaTagging.js +2 -10
- package/server/routes/sql.js +35 -10
- package/server/server.js +24 -0
- package/server/services/sqlite/dataBrowserService.js +25 -5
- package/server/services/sqlite/exportService.js +4 -2
- package/server/services/sqlite/introspection.js +2 -2
- package/server/services/sqlite/mediaTaggingService.js +166 -53
- package/server/services/sqlite/structureService.js +2 -2
- package/server/services/sqlite/tableDesigner/sql.js +19 -3
- package/server/services/storage/appStateStore.js +227 -87
- package/server/services/storage/queryHistoryChartUtils.js +19 -2
- package/server/utils/appPaths.js +55 -19
- package/server/utils/fileValidation.js +94 -8
- package/tailwind.config.cjs +73 -0
- package/tests/security-paths.test.js +84 -0
- package/tests/sql-identifier-safety.test.js +66 -0
- package/.npmingnore +0 -4
- package/changelog.md +0 -77
- package/docs/DESIGN_GUIDELINES.md +0 -36
- package/scripts/publish_brew.sh +0 -466
- package/scripts/publish_npm.sh +0 -241
- package/shortkeys.md +0 -5
package/frontend/js/store.js
CHANGED
|
@@ -29,7 +29,11 @@ const DEFAULT_SETTINGS = {
|
|
|
29
29
|
maxPageSize: 200,
|
|
30
30
|
csvDelimiter: ',',
|
|
31
31
|
};
|
|
32
|
+
const DEFAULT_DATA_PAGE_SIZE = 50;
|
|
32
33
|
const DATA_PAGE_SIZES = [25, 50, 100];
|
|
34
|
+
const DATA_ROW_SIZE_STORAGE_KEY = 'data_row_size';
|
|
35
|
+
const CHARTS_HISTORY_TAB_STORAGE_KEY = 'charts_history_tab';
|
|
36
|
+
const QUERY_HISTORY_TAB_STORAGE_KEY = 'query_history_tab';
|
|
33
37
|
const QUERY_HISTORY_PAGE_SIZE = 30;
|
|
34
38
|
const QUERY_HISTORY_RUN_LIMIT = 8;
|
|
35
39
|
const CHART_HEIGHT_PRESETS = new Set(['small', 'medium', 'large']);
|
|
@@ -46,6 +50,54 @@ let chartsLoadVersion = 0;
|
|
|
46
50
|
let chartsDetailLoadVersion = 0;
|
|
47
51
|
let mediaTaggingPreviewVersion = 0;
|
|
48
52
|
|
|
53
|
+
function readStoredDataPageSize(fallback = DEFAULT_DATA_PAGE_SIZE) {
|
|
54
|
+
try {
|
|
55
|
+
return normalizeDataPageSize(globalThis.localStorage?.getItem(DATA_ROW_SIZE_STORAGE_KEY), fallback);
|
|
56
|
+
} catch {
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function storeDataPageSize(pageSize) {
|
|
62
|
+
try {
|
|
63
|
+
globalThis.localStorage?.setItem(DATA_ROW_SIZE_STORAGE_KEY, String(pageSize));
|
|
64
|
+
} catch {
|
|
65
|
+
// Ignore unavailable browser storage; the in-memory setting still applies.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readStoredChartsHistoryTab(fallback = 'recent') {
|
|
70
|
+
try {
|
|
71
|
+
return normalizeChartsHistoryTab(globalThis.localStorage?.getItem(CHARTS_HISTORY_TAB_STORAGE_KEY) ?? fallback);
|
|
72
|
+
} catch {
|
|
73
|
+
return normalizeChartsHistoryTab(fallback);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function storeChartsHistoryTab(tab) {
|
|
78
|
+
try {
|
|
79
|
+
globalThis.localStorage?.setItem(CHARTS_HISTORY_TAB_STORAGE_KEY, normalizeChartsHistoryTab(tab));
|
|
80
|
+
} catch {
|
|
81
|
+
// Ignore unavailable browser storage; the in-memory setting still applies.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readStoredQueryHistoryTab(fallback = 'recent') {
|
|
86
|
+
try {
|
|
87
|
+
return normalizeQueryHistoryTab(globalThis.localStorage?.getItem(QUERY_HISTORY_TAB_STORAGE_KEY) ?? fallback);
|
|
88
|
+
} catch {
|
|
89
|
+
return normalizeQueryHistoryTab(fallback);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function storeQueryHistoryTab(tab) {
|
|
94
|
+
try {
|
|
95
|
+
globalThis.localStorage?.setItem(QUERY_HISTORY_TAB_STORAGE_KEY, normalizeQueryHistoryTab(tab));
|
|
96
|
+
} catch {
|
|
97
|
+
// Ignore unavailable browser storage; the in-memory setting still applies.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
49
101
|
const state = {
|
|
50
102
|
ready: false,
|
|
51
103
|
route: { name: 'landing', path: '/', params: {} },
|
|
@@ -79,7 +131,7 @@ const state = {
|
|
|
79
131
|
saving: false,
|
|
80
132
|
deleting: false,
|
|
81
133
|
page: 1,
|
|
82
|
-
pageSize:
|
|
134
|
+
pageSize: readStoredDataPageSize(),
|
|
83
135
|
sortColumn: null,
|
|
84
136
|
sortDirection: null,
|
|
85
137
|
searchQuery: '',
|
|
@@ -99,7 +151,7 @@ const state = {
|
|
|
99
151
|
historyLoading: false,
|
|
100
152
|
historyLoadingMore: false,
|
|
101
153
|
historyError: null,
|
|
102
|
-
historyTab:
|
|
154
|
+
historyTab: readStoredQueryHistoryTab(),
|
|
103
155
|
historySearchInput: '',
|
|
104
156
|
historySearch: '',
|
|
105
157
|
historyPageSize: QUERY_HISTORY_PAGE_SIZE,
|
|
@@ -124,9 +176,11 @@ const state = {
|
|
|
124
176
|
saveError: null,
|
|
125
177
|
},
|
|
126
178
|
charts: {
|
|
179
|
+
loaded: false,
|
|
127
180
|
queries: [],
|
|
128
181
|
loading: false,
|
|
129
182
|
error: null,
|
|
183
|
+
historyTab: readStoredChartsHistoryTab(),
|
|
130
184
|
selectedHistoryId: null,
|
|
131
185
|
chartHeightPreset: 'medium',
|
|
132
186
|
sqlExpanded: false,
|
|
@@ -666,9 +720,11 @@ function resetQueryHistoryState({ preserveSearch = true } = {}) {
|
|
|
666
720
|
function resetChartsState() {
|
|
667
721
|
chartsLoadVersion += 1;
|
|
668
722
|
chartsDetailLoadVersion += 1;
|
|
723
|
+
state.charts.loaded = false;
|
|
669
724
|
state.charts.queries = [];
|
|
670
725
|
state.charts.loading = false;
|
|
671
726
|
state.charts.error = null;
|
|
727
|
+
state.charts.historyTab = readStoredChartsHistoryTab(state.charts.historyTab);
|
|
672
728
|
state.charts.selectedHistoryId = null;
|
|
673
729
|
state.charts.chartHeightPreset = 'medium';
|
|
674
730
|
state.charts.sqlExpanded = false;
|
|
@@ -689,6 +745,10 @@ function normalizeChartsHeightPreset(value) {
|
|
|
689
745
|
return CHART_HEIGHT_PRESETS.has(normalizedValue) ? normalizedValue : 'medium';
|
|
690
746
|
}
|
|
691
747
|
|
|
748
|
+
function normalizeChartsHistoryTab(value) {
|
|
749
|
+
return ['recent', 'saved'].includes(value) ? value : 'recent';
|
|
750
|
+
}
|
|
751
|
+
|
|
692
752
|
function mergeQueryHistoryItemWithChartSummary(updatedItem, fallbackItem = null) {
|
|
693
753
|
if (!updatedItem) {
|
|
694
754
|
return updatedItem;
|
|
@@ -714,20 +774,24 @@ function syncQueryHistoryItem(updatedItem) {
|
|
|
714
774
|
return;
|
|
715
775
|
}
|
|
716
776
|
|
|
717
|
-
|
|
777
|
+
const updatedItemId = String(updatedItem.id);
|
|
778
|
+
|
|
779
|
+
state.editor.history = state.editor.history.map(entry => (String(entry.id) === updatedItemId ? updatedItem : entry));
|
|
718
780
|
|
|
719
|
-
if (state.editor.historyDetail?.id ===
|
|
781
|
+
if (String(state.editor.historyDetail?.id ?? '') === updatedItemId) {
|
|
720
782
|
state.editor.historyDetail = updatedItem;
|
|
721
783
|
}
|
|
722
784
|
|
|
723
785
|
const existingChartsEntry =
|
|
724
|
-
state.charts.queries.find(entry => entry.id ===
|
|
725
|
-
(state.charts.detail?.item?.id ===
|
|
786
|
+
state.charts.queries.find(entry => String(entry.id) === updatedItemId) ??
|
|
787
|
+
(String(state.charts.detail?.item?.id ?? '') === updatedItemId ? state.charts.detail.item : null);
|
|
726
788
|
const mergedChartsItem = mergeQueryHistoryItemWithChartSummary(updatedItem, existingChartsEntry);
|
|
727
789
|
|
|
728
|
-
state.charts.queries = state.charts.queries.map(entry =>
|
|
790
|
+
state.charts.queries = state.charts.queries.map(entry =>
|
|
791
|
+
String(entry.id) === updatedItemId ? mergedChartsItem : entry,
|
|
792
|
+
);
|
|
729
793
|
|
|
730
|
-
if (state.charts.detail?.item?.id ===
|
|
794
|
+
if (String(state.charts.detail?.item?.id ?? '') === updatedItemId) {
|
|
731
795
|
state.charts.detail = {
|
|
732
796
|
...state.charts.detail,
|
|
733
797
|
item: mergeQueryHistoryItemWithChartSummary(updatedItem, state.charts.detail.item),
|
|
@@ -1169,7 +1233,56 @@ async function loadChartsDetail(historyId) {
|
|
|
1169
1233
|
}
|
|
1170
1234
|
}
|
|
1171
1235
|
|
|
1172
|
-
|
|
1236
|
+
function getRequestedChartsHistoryId(route) {
|
|
1237
|
+
const requestedHistoryId = Number(route.params?.historyId ?? null);
|
|
1238
|
+
|
|
1239
|
+
return Number.isInteger(requestedHistoryId) && requestedHistoryId > 0 ? requestedHistoryId : null;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function resolveLoadableChartsHistoryId(route) {
|
|
1243
|
+
const requestedHistoryId = getRequestedChartsHistoryId(route);
|
|
1244
|
+
|
|
1245
|
+
if (!requestedHistoryId) {
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
return state.charts.queries.some(item => item.id === requestedHistoryId) ? requestedHistoryId : null;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function hasSettledChartsDetail(historyId) {
|
|
1253
|
+
if (state.charts.detailLoading || state.charts.resultLoading) {
|
|
1254
|
+
return false;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (historyId === null) {
|
|
1258
|
+
return (
|
|
1259
|
+
state.charts.selectedHistoryId === null &&
|
|
1260
|
+
!state.charts.detail &&
|
|
1261
|
+
!state.charts.result &&
|
|
1262
|
+
!state.charts.detailError &&
|
|
1263
|
+
!state.charts.resultError
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
return (
|
|
1268
|
+
state.charts.selectedHistoryId === historyId &&
|
|
1269
|
+
(state.charts.detail?.item?.id === historyId || Boolean(state.charts.detailError)) &&
|
|
1270
|
+
(Boolean(state.charts.result) || Boolean(state.charts.resultError))
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
async function loadCharts(version, route, options = {}) {
|
|
1275
|
+
if (!options.force && state.charts.loaded) {
|
|
1276
|
+
const historyId = resolveLoadableChartsHistoryId(route);
|
|
1277
|
+
|
|
1278
|
+
if (hasSettledChartsDetail(historyId)) {
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
await loadChartsDetail(historyId);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1173
1286
|
state.charts.loading = true;
|
|
1174
1287
|
state.charts.error = null;
|
|
1175
1288
|
emitChange();
|
|
@@ -1182,6 +1295,7 @@ async function loadCharts(version, route) {
|
|
|
1182
1295
|
}
|
|
1183
1296
|
|
|
1184
1297
|
state.charts.queries = response.data ?? [];
|
|
1298
|
+
state.charts.loaded = true;
|
|
1185
1299
|
state.charts.error = null;
|
|
1186
1300
|
} catch (error) {
|
|
1187
1301
|
if (version !== routeLoadVersion) {
|
|
@@ -1189,6 +1303,7 @@ async function loadCharts(version, route) {
|
|
|
1189
1303
|
}
|
|
1190
1304
|
|
|
1191
1305
|
state.charts.queries = [];
|
|
1306
|
+
state.charts.loaded = false;
|
|
1192
1307
|
state.charts.error = normalizeError(error);
|
|
1193
1308
|
} finally {
|
|
1194
1309
|
if (version === routeLoadVersion) {
|
|
@@ -1201,11 +1316,7 @@ async function loadCharts(version, route) {
|
|
|
1201
1316
|
return;
|
|
1202
1317
|
}
|
|
1203
1318
|
|
|
1204
|
-
|
|
1205
|
-
const canLoadRequestedHistory =
|
|
1206
|
-
Number.isInteger(requestedHistoryId) && state.charts.queries.some(item => item.id === requestedHistoryId);
|
|
1207
|
-
|
|
1208
|
-
await loadChartsDetail(canLoadRequestedHistory ? requestedHistoryId : null);
|
|
1319
|
+
await loadChartsDetail(resolveLoadableChartsHistoryId(route));
|
|
1209
1320
|
}
|
|
1210
1321
|
|
|
1211
1322
|
async function loadOverview(version) {
|
|
@@ -1280,7 +1391,7 @@ async function resolvePendingDataBrowserRow(version) {
|
|
|
1280
1391
|
|
|
1281
1392
|
async function loadDataTable(version) {
|
|
1282
1393
|
const tableName = state.dataBrowser.selectedTable;
|
|
1283
|
-
const pageSize = normalizeDataPageSize(state.dataBrowser.pageSize,
|
|
1394
|
+
const pageSize = normalizeDataPageSize(state.dataBrowser.pageSize, DEFAULT_DATA_PAGE_SIZE);
|
|
1284
1395
|
const page = Math.max(1, Number(state.dataBrowser.page) || 1);
|
|
1285
1396
|
const sortColumn = state.dataBrowser.sortColumn;
|
|
1286
1397
|
const sortDirection = normalizeSortDirection(state.dataBrowser.sortDirection);
|
|
@@ -1799,7 +1910,7 @@ function invalidateDatabaseCaches() {
|
|
|
1799
1910
|
state.mediaTagging.tagFormValues = {};
|
|
1800
1911
|
}
|
|
1801
1912
|
|
|
1802
|
-
async function loadRouteData(route) {
|
|
1913
|
+
async function loadRouteData(route, options = {}) {
|
|
1803
1914
|
clearRouteSlices();
|
|
1804
1915
|
|
|
1805
1916
|
if (requiresActiveDatabase(route.name) && !state.connections.active) {
|
|
@@ -1808,7 +1919,7 @@ async function loadRouteData(route) {
|
|
|
1808
1919
|
return;
|
|
1809
1920
|
}
|
|
1810
1921
|
|
|
1811
|
-
if (isMediaTaggingRouteName(route.name) && hasLoadedMediaTaggingForActiveConnection()) {
|
|
1922
|
+
if (!options.force && isMediaTaggingRouteName(route.name) && hasLoadedMediaTaggingForActiveConnection()) {
|
|
1812
1923
|
return;
|
|
1813
1924
|
}
|
|
1814
1925
|
|
|
@@ -1827,7 +1938,7 @@ async function loadRouteData(route) {
|
|
|
1827
1938
|
await loadData(version, route);
|
|
1828
1939
|
return;
|
|
1829
1940
|
case 'charts':
|
|
1830
|
-
await loadCharts(version, route);
|
|
1941
|
+
await loadCharts(version, route, options);
|
|
1831
1942
|
return;
|
|
1832
1943
|
case 'editor':
|
|
1833
1944
|
case 'editorResults':
|
|
@@ -2201,6 +2312,22 @@ export function setChartsHeightPreset(preset) {
|
|
|
2201
2312
|
emitChange();
|
|
2202
2313
|
}
|
|
2203
2314
|
|
|
2315
|
+
export function setChartsHistoryTab(tab) {
|
|
2316
|
+
const nextTab = normalizeChartsHistoryTab(
|
|
2317
|
+
String(tab ?? '')
|
|
2318
|
+
.trim()
|
|
2319
|
+
.toLowerCase(),
|
|
2320
|
+
);
|
|
2321
|
+
|
|
2322
|
+
if (state.charts.historyTab === nextTab) {
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
state.charts.historyTab = nextTab;
|
|
2327
|
+
storeChartsHistoryTab(nextTab);
|
|
2328
|
+
emitChange();
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2204
2331
|
export function updateCurrentQueryChartDraftField(field, value) {
|
|
2205
2332
|
if (state.modal?.kind !== 'chart-editor' || !state.modal.draft) {
|
|
2206
2333
|
return;
|
|
@@ -2529,6 +2656,7 @@ export async function setQueryHistoryTab(tab) {
|
|
|
2529
2656
|
}
|
|
2530
2657
|
|
|
2531
2658
|
state.editor.historyTab = normalizedTab;
|
|
2659
|
+
storeQueryHistoryTab(normalizedTab);
|
|
2532
2660
|
state.editor.historyActiveId = null;
|
|
2533
2661
|
clearQueryHistoryDetailState();
|
|
2534
2662
|
emitChange();
|
|
@@ -2625,16 +2753,25 @@ export async function runQueryHistoryItem(historyId) {
|
|
|
2625
2753
|
return executeCurrentQuery();
|
|
2626
2754
|
}
|
|
2627
2755
|
|
|
2628
|
-
export async function toggleQueryHistorySavedState(historyId, nextValue) {
|
|
2756
|
+
export async function toggleQueryHistorySavedState(historyId, nextValue, options = {}) {
|
|
2629
2757
|
try {
|
|
2630
2758
|
const response = await api.toggleQueryHistorySaved(historyId, nextValue);
|
|
2631
2759
|
syncQueryHistoryItem(response.data);
|
|
2632
|
-
|
|
2633
|
-
|
|
2760
|
+
if (options.toast !== false) {
|
|
2761
|
+
pushToast(response.message || 'Query save state updated.', 'muted');
|
|
2762
|
+
}
|
|
2763
|
+
if (options.notify !== false) {
|
|
2764
|
+
emitChange();
|
|
2765
|
+
}
|
|
2766
|
+
if (options.refresh !== false) {
|
|
2767
|
+
await refreshQueryHistoryState();
|
|
2768
|
+
}
|
|
2634
2769
|
return true;
|
|
2635
2770
|
} catch (error) {
|
|
2636
2771
|
state.editor.historyDetailError = normalizeError(error);
|
|
2637
|
-
|
|
2772
|
+
if (options.notify !== false) {
|
|
2773
|
+
emitChange();
|
|
2774
|
+
}
|
|
2638
2775
|
return false;
|
|
2639
2776
|
}
|
|
2640
2777
|
}
|
|
@@ -3340,6 +3477,7 @@ export async function setDataPageSize(pageSize) {
|
|
|
3340
3477
|
}
|
|
3341
3478
|
|
|
3342
3479
|
state.dataBrowser.pageSize = normalizedPageSize;
|
|
3480
|
+
storeDataPageSize(normalizedPageSize);
|
|
3343
3481
|
state.dataBrowser.page = 1;
|
|
3344
3482
|
clearDataBrowserRowSelectionState();
|
|
3345
3483
|
state.dataBrowser.saveError = null;
|
|
@@ -3735,7 +3873,7 @@ export function sortEditorResultsByColumn(columnName) {
|
|
|
3735
3873
|
}
|
|
3736
3874
|
|
|
3737
3875
|
export async function refreshCurrentRoute() {
|
|
3738
|
-
await loadRouteData(state.route);
|
|
3876
|
+
await loadRouteData(state.route, { force: true });
|
|
3739
3877
|
}
|
|
3740
3878
|
|
|
3741
3879
|
export function showToast(message, tone = 'muted') {
|
|
@@ -713,13 +713,19 @@ function buildImportedInsertPreviewSql(draft, maxRows = 3) {
|
|
|
713
713
|
}
|
|
714
714
|
|
|
715
715
|
const columnSql = importedColumns.map((column) => quoteIdentifier(column.name)).join(", ");
|
|
716
|
-
const insertPrefix =
|
|
716
|
+
const insertPrefix = [
|
|
717
|
+
"INSERT INTO",
|
|
718
|
+
quoteIdentifier(draft.tableName),
|
|
719
|
+
"(",
|
|
720
|
+
columnSql,
|
|
721
|
+
") VALUES",
|
|
722
|
+
].join(" ");
|
|
717
723
|
const previewStatements = draft.importedCsvRows.slice(0, maxRows).map((row) => {
|
|
718
724
|
const valueSql = importedColumns
|
|
719
725
|
.map((column) => formatImportedCellValueForSql(column, row[column.importedValueIndex] ?? ""))
|
|
720
726
|
.join(", ");
|
|
721
727
|
|
|
722
|
-
return
|
|
728
|
+
return [insertPrefix, "(", valueSql, ");"].join(" ");
|
|
723
729
|
});
|
|
724
730
|
|
|
725
731
|
if (draft.importedCsvRows.length > maxRows) {
|
|
@@ -20,10 +20,12 @@ function renderMissingDatabase() {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function renderChartsList(state) {
|
|
23
|
-
const
|
|
23
|
+
const activeTab = ['recent', 'saved'].includes(state.charts.historyTab) ? state.charts.historyTab : 'recent';
|
|
24
|
+
const allQueries = state.charts.queries ?? [];
|
|
25
|
+
const queries = activeTab === 'saved' ? allQueries.filter(item => item.isSaved) : allQueries;
|
|
24
26
|
const selectedHistoryId = Number(state.charts.selectedHistoryId);
|
|
25
27
|
|
|
26
|
-
if (state.charts.loading && !
|
|
28
|
+
if (state.charts.loading && !allQueries.length) {
|
|
27
29
|
return `
|
|
28
30
|
<div class="flex h-full items-center justify-center px-6 text-center text-on-surface-variant/45">
|
|
29
31
|
<div>
|
|
@@ -34,7 +36,7 @@ function renderChartsList(state) {
|
|
|
34
36
|
`;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
if (state.charts.error && !
|
|
39
|
+
if (state.charts.error && !allQueries.length) {
|
|
38
40
|
return `
|
|
39
41
|
<div class="p-5">
|
|
40
42
|
<div class="border border-error/30 bg-error-container/20 px-4 py-4 text-sm text-error">
|
|
@@ -45,15 +47,23 @@ function renderChartsList(state) {
|
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
if (!queries.length) {
|
|
50
|
+
const isSavedTab = activeTab === 'saved';
|
|
51
|
+
|
|
48
52
|
return `
|
|
49
53
|
<div class="flex h-full items-center justify-center px-6 text-center">
|
|
50
54
|
<div>
|
|
51
|
-
<span class="material-symbols-outlined mb-3 text-4xl text-on-surface-variant/25"
|
|
55
|
+
<span class="material-symbols-outlined mb-3 text-4xl text-on-surface-variant/25">${
|
|
56
|
+
isSavedTab ? 'bookmark' : 'query_stats'
|
|
57
|
+
}</span>
|
|
52
58
|
<p class="font-headline text-lg font-black uppercase tracking-tight text-on-surface">
|
|
53
|
-
No Chartable Queries
|
|
59
|
+
${isSavedTab ? 'No Saved Charts Queries' : 'No Chartable Queries'}
|
|
54
60
|
</p>
|
|
55
61
|
<p class="mt-2 max-w-xs text-sm leading-6 text-on-surface-variant/60">
|
|
56
|
-
|
|
62
|
+
${
|
|
63
|
+
isSavedTab
|
|
64
|
+
? 'Save chartable queries from this list or from the SQL Editor to keep them here.'
|
|
65
|
+
: 'Run SELECT queries in the SQL Editor first. They will appear here automatically.'
|
|
66
|
+
}
|
|
57
67
|
</p>
|
|
58
68
|
</div>
|
|
59
69
|
</div>
|
|
@@ -66,36 +76,67 @@ function renderChartsList(state) {
|
|
|
66
76
|
${queries
|
|
67
77
|
.map(
|
|
68
78
|
item => `
|
|
69
|
-
<
|
|
70
|
-
class="w-full border
|
|
71
|
-
selectedHistoryId === item.id
|
|
79
|
+
<article
|
|
80
|
+
class="group w-full border transition-colors ${
|
|
81
|
+
selectedHistoryId === Number(item.id)
|
|
72
82
|
? 'border-primary-container/30 bg-surface-container-high'
|
|
73
83
|
: 'border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high'
|
|
74
84
|
}"
|
|
75
|
-
data-
|
|
76
|
-
data-to="/charts/${encodeURIComponent(item.id)}"
|
|
77
|
-
type="button"
|
|
85
|
+
data-charts-history-item
|
|
78
86
|
>
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}"
|
|
83
|
-
|
|
87
|
+
<button
|
|
88
|
+
class="w-full px-4 py-3 text-left transition-colors group-hover:bg-surface-container-high"
|
|
89
|
+
data-action="navigate"
|
|
90
|
+
data-history-id="${escapeHtml(item.id)}"
|
|
91
|
+
data-to="/charts/${encodeURIComponent(item.id)}"
|
|
92
|
+
type="button"
|
|
93
|
+
>
|
|
94
|
+
<div class="flex items-start justify-between gap-3">
|
|
95
|
+
<div class="min-w-0 flex-1 truncate font-mono text-xs ${
|
|
96
|
+
selectedHistoryId === Number(item.id) ? 'text-primary-container' : 'text-on-surface'
|
|
97
|
+
}" data-charts-history-title>
|
|
98
|
+
${escapeHtml(item.displayTitle)}
|
|
99
|
+
</div>
|
|
100
|
+
<div class="flex shrink-0 flex-wrap justify-end gap-1">
|
|
101
|
+
<span class="inline-flex" data-charts-saved-badge ${item.isSaved ? '' : 'hidden'}>
|
|
102
|
+
${renderStatusBadge('saved', 'primary')}
|
|
103
|
+
</span>
|
|
104
|
+
${
|
|
105
|
+
item.chartTypes?.length
|
|
106
|
+
? item.chartTypes
|
|
107
|
+
.map(chartType => renderStatusBadge(getQueryChartTypeLabel(chartType), 'primary'))
|
|
108
|
+
.join('')
|
|
109
|
+
: renderStatusBadge('None', 'muted')
|
|
110
|
+
}
|
|
111
|
+
</div>
|
|
84
112
|
</div>
|
|
85
|
-
<div class="
|
|
86
|
-
${
|
|
87
|
-
item.chartTypes?.length
|
|
88
|
-
? item.chartTypes
|
|
89
|
-
.map(chartType => renderStatusBadge(getQueryChartTypeLabel(chartType), 'primary'))
|
|
90
|
-
.join('')
|
|
91
|
-
: renderStatusBadge('None', 'muted')
|
|
92
|
-
}
|
|
113
|
+
<div class="mt-1 truncate text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">
|
|
114
|
+
${escapeHtml(item.previewSql)}
|
|
93
115
|
</div>
|
|
116
|
+
</button>
|
|
117
|
+
<div class="flex items-center justify-end border-t border-outline-variant/10 px-3 py-2 transition-colors group-hover:bg-surface-container-high">
|
|
118
|
+
<button
|
|
119
|
+
aria-label="Open chart query"
|
|
120
|
+
class="min-h-[var(--control-height)] flex-1 self-stretch"
|
|
121
|
+
data-action="navigate"
|
|
122
|
+
data-history-id="${escapeHtml(item.id)}"
|
|
123
|
+
data-to="/charts/${encodeURIComponent(item.id)}"
|
|
124
|
+
type="button"
|
|
125
|
+
></button>
|
|
126
|
+
<button
|
|
127
|
+
class="query-history-icon-button ${item.isSaved ? 'is-active' : ''}"
|
|
128
|
+
data-action="toggle-charts-query-history-saved"
|
|
129
|
+
data-history-id="${escapeHtml(item.id)}"
|
|
130
|
+
data-next-value="${item.isSaved ? 'false' : 'true'}"
|
|
131
|
+
title="${item.isSaved ? 'Remove from saved' : 'Save query'}"
|
|
132
|
+
type="button"
|
|
133
|
+
>
|
|
134
|
+
<span class="material-symbols-outlined text-[18px]">
|
|
135
|
+
${item.isSaved ? 'bookmark' : 'bookmark_add'}
|
|
136
|
+
</span>
|
|
137
|
+
</button>
|
|
94
138
|
</div>
|
|
95
|
-
|
|
96
|
-
${escapeHtml(item.previewSql)}
|
|
97
|
-
</div>
|
|
98
|
-
</button>
|
|
139
|
+
</article>
|
|
99
140
|
`,
|
|
100
141
|
)
|
|
101
142
|
.join('')}
|
|
@@ -104,6 +145,37 @@ function renderChartsList(state) {
|
|
|
104
145
|
`;
|
|
105
146
|
}
|
|
106
147
|
|
|
148
|
+
function renderChartsHistoryTabs(state) {
|
|
149
|
+
const activeTab = ['recent', 'saved'].includes(state.charts.historyTab) ? state.charts.historyTab : 'recent';
|
|
150
|
+
const savedCount = (state.charts.queries ?? []).filter(item => item.isSaved).length;
|
|
151
|
+
const tabs = [
|
|
152
|
+
{ id: 'recent', label: 'Recent', count: state.charts.queries?.length ?? 0 },
|
|
153
|
+
{ id: 'saved', label: 'Saved', count: savedCount },
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
return `
|
|
157
|
+
<div class="mt-4 flex items-center gap-2">
|
|
158
|
+
${tabs
|
|
159
|
+
.map(
|
|
160
|
+
tab => `
|
|
161
|
+
<button
|
|
162
|
+
class="query-history-tab ${activeTab === tab.id ? 'is-active' : ''}"
|
|
163
|
+
data-action="set-charts-history-tab"
|
|
164
|
+
data-tab="${escapeHtml(tab.id)}"
|
|
165
|
+
type="button"
|
|
166
|
+
>
|
|
167
|
+
${escapeHtml(tab.label)}
|
|
168
|
+
</button>
|
|
169
|
+
`,
|
|
170
|
+
)
|
|
171
|
+
.join('')}
|
|
172
|
+
<span class="ml-auto text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/50">
|
|
173
|
+
<span data-charts-history-count>${escapeHtml(String(tabs.find(tab => tab.id === activeTab)?.count ?? 0))}</span>
|
|
174
|
+
</span>
|
|
175
|
+
</div>
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
|
|
107
179
|
function renderEmptyChartDetail() {
|
|
108
180
|
return `
|
|
109
181
|
<div class="flex flex-1 items-center justify-center px-8 py-10 text-center">
|
|
@@ -302,52 +374,32 @@ function renderChartSurface(chart, state, analysis) {
|
|
|
302
374
|
function renderChartCard(chart, state, analysis) {
|
|
303
375
|
const sizeClass = resolveChartCardSizeClass(state);
|
|
304
376
|
|
|
305
|
-
return
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
data-chart-id="${escapeHtml(chart.id)}"
|
|
329
|
-
type="button"
|
|
330
|
-
>
|
|
331
|
-
Delete
|
|
332
|
-
</button>
|
|
333
|
-
<button
|
|
334
|
-
class="standard-button"
|
|
335
|
-
data-action="export-query-chart-png"
|
|
336
|
-
data-chart-id="${escapeHtml(chart.id)}"
|
|
337
|
-
type="button"
|
|
338
|
-
>
|
|
339
|
-
Export PNG
|
|
340
|
-
</button>
|
|
341
|
-
</div>
|
|
342
|
-
</header>
|
|
343
|
-
<div class="query-chart-card__body">
|
|
344
|
-
${renderChartSurface(chart, state, analysis)}
|
|
345
|
-
</div>
|
|
346
|
-
</article>
|
|
347
|
-
`;
|
|
377
|
+
return [
|
|
378
|
+
'<article class="query-chart-card ',
|
|
379
|
+
sizeClass,
|
|
380
|
+
'"><header class="query-chart-card__header"><div class="min-w-0">',
|
|
381
|
+
'<div class="flex flex-wrap items-center gap-2">',
|
|
382
|
+
'<h3 class="truncate font-headline text-xl font-black uppercase tracking-tight text-on-surface">',
|
|
383
|
+
escapeHtml(chart.name),
|
|
384
|
+
"</h3>",
|
|
385
|
+
renderStatusBadge(getQueryChartTypeLabel(chart.chartType), 'primary'),
|
|
386
|
+
'</div></div><div class="flex flex-wrap items-center gap-2">',
|
|
387
|
+
'<button class="standard-button" data-action="open-edit-query-chart-modal" data-chart-id="',
|
|
388
|
+
escapeHtml(chart.id),
|
|
389
|
+
'" type="button">Edit</button>',
|
|
390
|
+
'<button class="delete-button" data-action="open-delete-query-chart-modal" data-chart-id="',
|
|
391
|
+
escapeHtml(chart.id),
|
|
392
|
+
'" type="button">Delete</button>',
|
|
393
|
+
'<button class="standard-button" data-action="export-query-chart-png" data-chart-id="',
|
|
394
|
+
escapeHtml(chart.id),
|
|
395
|
+
'" type="button">Export PNG</button>',
|
|
396
|
+
'</div></header><div class="query-chart-card__body">',
|
|
397
|
+
renderChartSurface(chart, state, analysis),
|
|
398
|
+
"</div></article>",
|
|
399
|
+
].join("");
|
|
348
400
|
}
|
|
349
401
|
|
|
350
|
-
function renderChartsDetail(state) {
|
|
402
|
+
export function renderChartsDetail(state) {
|
|
351
403
|
const detail = state.charts.detail;
|
|
352
404
|
const selectedHistoryId = state.charts.selectedHistoryId;
|
|
353
405
|
const historyVisible = state.editor.historyPanelVisible !== false;
|
|
@@ -485,6 +537,7 @@ export function renderChartsView(state) {
|
|
|
485
537
|
<h2 class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
486
538
|
Charts
|
|
487
539
|
</h2>
|
|
540
|
+
${renderChartsHistoryTabs(state)}
|
|
488
541
|
</div>
|
|
489
542
|
${renderChartsList(state)}
|
|
490
543
|
</aside>
|