sqlite-hub 0.9.1 → 0.9.3
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/changelog.md +7 -0
- package/frontend/index.html +21 -20
- package/frontend/js/app.js +226 -4
- package/frontend/js/components/modal.js +13 -4
- package/frontend/js/components/queryHistoryPanel.js +9 -1
- package/frontend/js/components/rowEditorPanel.js +48 -25
- package/frontend/js/lib/queryChartOptions.js +5 -1
- package/frontend/js/lib/queryCharts.js +50 -2
- package/frontend/js/store.js +124 -22
- package/frontend/js/views/charts.js +103 -30
- package/frontend/js/views/data.js +31 -1
- package/frontend/js/views/editor.js +1 -0
- 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/tokens.css +47 -4
- package/frontend/styles/utilities.css +21 -0
- package/frontend/styles/views.css +94 -86
- package/package.json +1 -1
- package/server/services/storage/queryHistoryChartUtils.js +19 -2
package/frontend/js/store.js
CHANGED
|
@@ -29,7 +29,9 @@ 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';
|
|
33
35
|
const QUERY_HISTORY_PAGE_SIZE = 30;
|
|
34
36
|
const QUERY_HISTORY_RUN_LIMIT = 8;
|
|
35
37
|
const CHART_HEIGHT_PRESETS = new Set(['small', 'medium', 'large']);
|
|
@@ -46,6 +48,22 @@ let chartsLoadVersion = 0;
|
|
|
46
48
|
let chartsDetailLoadVersion = 0;
|
|
47
49
|
let mediaTaggingPreviewVersion = 0;
|
|
48
50
|
|
|
51
|
+
function readStoredDataPageSize(fallback = DEFAULT_DATA_PAGE_SIZE) {
|
|
52
|
+
try {
|
|
53
|
+
return normalizeDataPageSize(globalThis.localStorage?.getItem(DATA_ROW_SIZE_STORAGE_KEY), fallback);
|
|
54
|
+
} catch {
|
|
55
|
+
return fallback;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function storeDataPageSize(pageSize) {
|
|
60
|
+
try {
|
|
61
|
+
globalThis.localStorage?.setItem(DATA_ROW_SIZE_STORAGE_KEY, String(pageSize));
|
|
62
|
+
} catch {
|
|
63
|
+
// Ignore unavailable browser storage; the in-memory setting still applies.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
49
67
|
const state = {
|
|
50
68
|
ready: false,
|
|
51
69
|
route: { name: 'landing', path: '/', params: {} },
|
|
@@ -79,7 +97,7 @@ const state = {
|
|
|
79
97
|
saving: false,
|
|
80
98
|
deleting: false,
|
|
81
99
|
page: 1,
|
|
82
|
-
pageSize:
|
|
100
|
+
pageSize: readStoredDataPageSize(),
|
|
83
101
|
sortColumn: null,
|
|
84
102
|
sortDirection: null,
|
|
85
103
|
searchQuery: '',
|
|
@@ -124,9 +142,11 @@ const state = {
|
|
|
124
142
|
saveError: null,
|
|
125
143
|
},
|
|
126
144
|
charts: {
|
|
145
|
+
loaded: false,
|
|
127
146
|
queries: [],
|
|
128
147
|
loading: false,
|
|
129
148
|
error: null,
|
|
149
|
+
historyTab: 'recent',
|
|
130
150
|
selectedHistoryId: null,
|
|
131
151
|
chartHeightPreset: 'medium',
|
|
132
152
|
sqlExpanded: false,
|
|
@@ -666,9 +686,11 @@ function resetQueryHistoryState({ preserveSearch = true } = {}) {
|
|
|
666
686
|
function resetChartsState() {
|
|
667
687
|
chartsLoadVersion += 1;
|
|
668
688
|
chartsDetailLoadVersion += 1;
|
|
689
|
+
state.charts.loaded = false;
|
|
669
690
|
state.charts.queries = [];
|
|
670
691
|
state.charts.loading = false;
|
|
671
692
|
state.charts.error = null;
|
|
693
|
+
state.charts.historyTab = 'recent';
|
|
672
694
|
state.charts.selectedHistoryId = null;
|
|
673
695
|
state.charts.chartHeightPreset = 'medium';
|
|
674
696
|
state.charts.sqlExpanded = false;
|
|
@@ -689,6 +711,10 @@ function normalizeChartsHeightPreset(value) {
|
|
|
689
711
|
return CHART_HEIGHT_PRESETS.has(normalizedValue) ? normalizedValue : 'medium';
|
|
690
712
|
}
|
|
691
713
|
|
|
714
|
+
function normalizeChartsHistoryTab(value) {
|
|
715
|
+
return ['recent', 'saved'].includes(value) ? value : 'recent';
|
|
716
|
+
}
|
|
717
|
+
|
|
692
718
|
function mergeQueryHistoryItemWithChartSummary(updatedItem, fallbackItem = null) {
|
|
693
719
|
if (!updatedItem) {
|
|
694
720
|
return updatedItem;
|
|
@@ -714,20 +740,24 @@ function syncQueryHistoryItem(updatedItem) {
|
|
|
714
740
|
return;
|
|
715
741
|
}
|
|
716
742
|
|
|
717
|
-
|
|
743
|
+
const updatedItemId = String(updatedItem.id);
|
|
718
744
|
|
|
719
|
-
|
|
745
|
+
state.editor.history = state.editor.history.map(entry => (String(entry.id) === updatedItemId ? updatedItem : entry));
|
|
746
|
+
|
|
747
|
+
if (String(state.editor.historyDetail?.id ?? '') === updatedItemId) {
|
|
720
748
|
state.editor.historyDetail = updatedItem;
|
|
721
749
|
}
|
|
722
750
|
|
|
723
751
|
const existingChartsEntry =
|
|
724
|
-
state.charts.queries.find(entry => entry.id ===
|
|
725
|
-
(state.charts.detail?.item?.id ===
|
|
752
|
+
state.charts.queries.find(entry => String(entry.id) === updatedItemId) ??
|
|
753
|
+
(String(state.charts.detail?.item?.id ?? '') === updatedItemId ? state.charts.detail.item : null);
|
|
726
754
|
const mergedChartsItem = mergeQueryHistoryItemWithChartSummary(updatedItem, existingChartsEntry);
|
|
727
755
|
|
|
728
|
-
state.charts.queries = state.charts.queries.map(entry =>
|
|
756
|
+
state.charts.queries = state.charts.queries.map(entry =>
|
|
757
|
+
String(entry.id) === updatedItemId ? mergedChartsItem : entry,
|
|
758
|
+
);
|
|
729
759
|
|
|
730
|
-
if (state.charts.detail?.item?.id ===
|
|
760
|
+
if (String(state.charts.detail?.item?.id ?? '') === updatedItemId) {
|
|
731
761
|
state.charts.detail = {
|
|
732
762
|
...state.charts.detail,
|
|
733
763
|
item: mergeQueryHistoryItemWithChartSummary(updatedItem, state.charts.detail.item),
|
|
@@ -1169,7 +1199,56 @@ async function loadChartsDetail(historyId) {
|
|
|
1169
1199
|
}
|
|
1170
1200
|
}
|
|
1171
1201
|
|
|
1172
|
-
|
|
1202
|
+
function getRequestedChartsHistoryId(route) {
|
|
1203
|
+
const requestedHistoryId = Number(route.params?.historyId ?? null);
|
|
1204
|
+
|
|
1205
|
+
return Number.isInteger(requestedHistoryId) && requestedHistoryId > 0 ? requestedHistoryId : null;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function resolveLoadableChartsHistoryId(route) {
|
|
1209
|
+
const requestedHistoryId = getRequestedChartsHistoryId(route);
|
|
1210
|
+
|
|
1211
|
+
if (!requestedHistoryId) {
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
return state.charts.queries.some(item => item.id === requestedHistoryId) ? requestedHistoryId : null;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function hasSettledChartsDetail(historyId) {
|
|
1219
|
+
if (state.charts.detailLoading || state.charts.resultLoading) {
|
|
1220
|
+
return false;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (historyId === null) {
|
|
1224
|
+
return (
|
|
1225
|
+
state.charts.selectedHistoryId === null &&
|
|
1226
|
+
!state.charts.detail &&
|
|
1227
|
+
!state.charts.result &&
|
|
1228
|
+
!state.charts.detailError &&
|
|
1229
|
+
!state.charts.resultError
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
return (
|
|
1234
|
+
state.charts.selectedHistoryId === historyId &&
|
|
1235
|
+
(state.charts.detail?.item?.id === historyId || Boolean(state.charts.detailError)) &&
|
|
1236
|
+
(Boolean(state.charts.result) || Boolean(state.charts.resultError))
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
async function loadCharts(version, route, options = {}) {
|
|
1241
|
+
if (!options.force && state.charts.loaded) {
|
|
1242
|
+
const historyId = resolveLoadableChartsHistoryId(route);
|
|
1243
|
+
|
|
1244
|
+
if (hasSettledChartsDetail(historyId)) {
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
await loadChartsDetail(historyId);
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1173
1252
|
state.charts.loading = true;
|
|
1174
1253
|
state.charts.error = null;
|
|
1175
1254
|
emitChange();
|
|
@@ -1182,6 +1261,7 @@ async function loadCharts(version, route) {
|
|
|
1182
1261
|
}
|
|
1183
1262
|
|
|
1184
1263
|
state.charts.queries = response.data ?? [];
|
|
1264
|
+
state.charts.loaded = true;
|
|
1185
1265
|
state.charts.error = null;
|
|
1186
1266
|
} catch (error) {
|
|
1187
1267
|
if (version !== routeLoadVersion) {
|
|
@@ -1189,6 +1269,7 @@ async function loadCharts(version, route) {
|
|
|
1189
1269
|
}
|
|
1190
1270
|
|
|
1191
1271
|
state.charts.queries = [];
|
|
1272
|
+
state.charts.loaded = false;
|
|
1192
1273
|
state.charts.error = normalizeError(error);
|
|
1193
1274
|
} finally {
|
|
1194
1275
|
if (version === routeLoadVersion) {
|
|
@@ -1201,11 +1282,7 @@ async function loadCharts(version, route) {
|
|
|
1201
1282
|
return;
|
|
1202
1283
|
}
|
|
1203
1284
|
|
|
1204
|
-
|
|
1205
|
-
const canLoadRequestedHistory =
|
|
1206
|
-
Number.isInteger(requestedHistoryId) && state.charts.queries.some(item => item.id === requestedHistoryId);
|
|
1207
|
-
|
|
1208
|
-
await loadChartsDetail(canLoadRequestedHistory ? requestedHistoryId : null);
|
|
1285
|
+
await loadChartsDetail(resolveLoadableChartsHistoryId(route));
|
|
1209
1286
|
}
|
|
1210
1287
|
|
|
1211
1288
|
async function loadOverview(version) {
|
|
@@ -1280,7 +1357,7 @@ async function resolvePendingDataBrowserRow(version) {
|
|
|
1280
1357
|
|
|
1281
1358
|
async function loadDataTable(version) {
|
|
1282
1359
|
const tableName = state.dataBrowser.selectedTable;
|
|
1283
|
-
const pageSize = normalizeDataPageSize(state.dataBrowser.pageSize,
|
|
1360
|
+
const pageSize = normalizeDataPageSize(state.dataBrowser.pageSize, DEFAULT_DATA_PAGE_SIZE);
|
|
1284
1361
|
const page = Math.max(1, Number(state.dataBrowser.page) || 1);
|
|
1285
1362
|
const sortColumn = state.dataBrowser.sortColumn;
|
|
1286
1363
|
const sortDirection = normalizeSortDirection(state.dataBrowser.sortDirection);
|
|
@@ -1799,7 +1876,7 @@ function invalidateDatabaseCaches() {
|
|
|
1799
1876
|
state.mediaTagging.tagFormValues = {};
|
|
1800
1877
|
}
|
|
1801
1878
|
|
|
1802
|
-
async function loadRouteData(route) {
|
|
1879
|
+
async function loadRouteData(route, options = {}) {
|
|
1803
1880
|
clearRouteSlices();
|
|
1804
1881
|
|
|
1805
1882
|
if (requiresActiveDatabase(route.name) && !state.connections.active) {
|
|
@@ -1808,7 +1885,7 @@ async function loadRouteData(route) {
|
|
|
1808
1885
|
return;
|
|
1809
1886
|
}
|
|
1810
1887
|
|
|
1811
|
-
if (isMediaTaggingRouteName(route.name) && hasLoadedMediaTaggingForActiveConnection()) {
|
|
1888
|
+
if (!options.force && isMediaTaggingRouteName(route.name) && hasLoadedMediaTaggingForActiveConnection()) {
|
|
1812
1889
|
return;
|
|
1813
1890
|
}
|
|
1814
1891
|
|
|
@@ -1827,7 +1904,7 @@ async function loadRouteData(route) {
|
|
|
1827
1904
|
await loadData(version, route);
|
|
1828
1905
|
return;
|
|
1829
1906
|
case 'charts':
|
|
1830
|
-
await loadCharts(version, route);
|
|
1907
|
+
await loadCharts(version, route, options);
|
|
1831
1908
|
return;
|
|
1832
1909
|
case 'editor':
|
|
1833
1910
|
case 'editorResults':
|
|
@@ -2201,6 +2278,21 @@ export function setChartsHeightPreset(preset) {
|
|
|
2201
2278
|
emitChange();
|
|
2202
2279
|
}
|
|
2203
2280
|
|
|
2281
|
+
export function setChartsHistoryTab(tab) {
|
|
2282
|
+
const nextTab = normalizeChartsHistoryTab(
|
|
2283
|
+
String(tab ?? '')
|
|
2284
|
+
.trim()
|
|
2285
|
+
.toLowerCase(),
|
|
2286
|
+
);
|
|
2287
|
+
|
|
2288
|
+
if (state.charts.historyTab === nextTab) {
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
state.charts.historyTab = nextTab;
|
|
2293
|
+
emitChange();
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2204
2296
|
export function updateCurrentQueryChartDraftField(field, value) {
|
|
2205
2297
|
if (state.modal?.kind !== 'chart-editor' || !state.modal.draft) {
|
|
2206
2298
|
return;
|
|
@@ -2625,16 +2717,25 @@ export async function runQueryHistoryItem(historyId) {
|
|
|
2625
2717
|
return executeCurrentQuery();
|
|
2626
2718
|
}
|
|
2627
2719
|
|
|
2628
|
-
export async function toggleQueryHistorySavedState(historyId, nextValue) {
|
|
2720
|
+
export async function toggleQueryHistorySavedState(historyId, nextValue, options = {}) {
|
|
2629
2721
|
try {
|
|
2630
2722
|
const response = await api.toggleQueryHistorySaved(historyId, nextValue);
|
|
2631
2723
|
syncQueryHistoryItem(response.data);
|
|
2632
|
-
|
|
2633
|
-
|
|
2724
|
+
if (options.toast !== false) {
|
|
2725
|
+
pushToast(response.message || 'Query save state updated.', 'muted');
|
|
2726
|
+
}
|
|
2727
|
+
if (options.notify !== false) {
|
|
2728
|
+
emitChange();
|
|
2729
|
+
}
|
|
2730
|
+
if (options.refresh !== false) {
|
|
2731
|
+
await refreshQueryHistoryState();
|
|
2732
|
+
}
|
|
2634
2733
|
return true;
|
|
2635
2734
|
} catch (error) {
|
|
2636
2735
|
state.editor.historyDetailError = normalizeError(error);
|
|
2637
|
-
|
|
2736
|
+
if (options.notify !== false) {
|
|
2737
|
+
emitChange();
|
|
2738
|
+
}
|
|
2638
2739
|
return false;
|
|
2639
2740
|
}
|
|
2640
2741
|
}
|
|
@@ -3340,6 +3441,7 @@ export async function setDataPageSize(pageSize) {
|
|
|
3340
3441
|
}
|
|
3341
3442
|
|
|
3342
3443
|
state.dataBrowser.pageSize = normalizedPageSize;
|
|
3444
|
+
storeDataPageSize(normalizedPageSize);
|
|
3343
3445
|
state.dataBrowser.page = 1;
|
|
3344
3446
|
clearDataBrowserRowSelectionState();
|
|
3345
3447
|
state.dataBrowser.saveError = null;
|
|
@@ -3735,7 +3837,7 @@ export function sortEditorResultsByColumn(columnName) {
|
|
|
3735
3837
|
}
|
|
3736
3838
|
|
|
3737
3839
|
export async function refreshCurrentRoute() {
|
|
3738
|
-
await loadRouteData(state.route);
|
|
3840
|
+
await loadRouteData(state.route, { force: true });
|
|
3739
3841
|
}
|
|
3740
3842
|
|
|
3741
3843
|
export function showToast(message, tone = 'muted') {
|
|
@@ -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">
|
|
@@ -347,7 +419,7 @@ function renderChartCard(chart, state, analysis) {
|
|
|
347
419
|
`;
|
|
348
420
|
}
|
|
349
421
|
|
|
350
|
-
function renderChartsDetail(state) {
|
|
422
|
+
export function renderChartsDetail(state) {
|
|
351
423
|
const detail = state.charts.detail;
|
|
352
424
|
const selectedHistoryId = state.charts.selectedHistoryId;
|
|
353
425
|
const historyVisible = state.editor.historyPanelVisible !== false;
|
|
@@ -485,6 +557,7 @@ export function renderChartsView(state) {
|
|
|
485
557
|
<h2 class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
486
558
|
Charts
|
|
487
559
|
</h2>
|
|
560
|
+
${renderChartsHistoryTabs(state)}
|
|
488
561
|
</div>
|
|
489
562
|
${renderChartsList(state)}
|
|
490
563
|
</aside>
|
|
@@ -449,7 +449,36 @@ export function renderDataRowEditorPanel(state) {
|
|
|
449
449
|
(foreignKey.mappings ?? []).map(mapping => String(mapping.from ?? '').trim()).filter(Boolean),
|
|
450
450
|
),
|
|
451
451
|
);
|
|
452
|
-
const
|
|
452
|
+
const getColumnTypeBadge = column => String(column.declaredType || column.affinity || 'BLOB').trim().toUpperCase();
|
|
453
|
+
const getColumnNumberInputMeta = column => {
|
|
454
|
+
const affinity = String(column.affinity ?? '').toUpperCase();
|
|
455
|
+
|
|
456
|
+
if (affinity === 'INTEGER') {
|
|
457
|
+
return { inputType: 'number', numberStep: '1' };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (affinity === 'NUMERIC') {
|
|
461
|
+
return { inputType: 'number', numberStep: 'any' };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return {};
|
|
465
|
+
};
|
|
466
|
+
const getColumnBadges = column => {
|
|
467
|
+
const badges = [{ label: getColumnTypeBadge(column), tone: 'type' }];
|
|
468
|
+
|
|
469
|
+
if (column.primaryKeyPosition > 0) {
|
|
470
|
+
badges.push({
|
|
471
|
+
label: column.primaryKeyPosition > 1 ? `PK ${column.primaryKeyPosition}` : 'PK',
|
|
472
|
+
tone: 'primary-key',
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (foreignKeyColumnNames.has(column.name)) {
|
|
477
|
+
badges.push({ label: 'FK', tone: 'foreign-key' });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return badges;
|
|
481
|
+
};
|
|
453
482
|
|
|
454
483
|
const identityColumns = table.identityStrategy?.type === 'primaryKey' ? (table.identityStrategy.columns ?? []) : [];
|
|
455
484
|
const editableColumns = (table.columnMeta ?? []).filter(column => {
|
|
@@ -502,6 +531,7 @@ export function renderDataRowEditorPanel(state) {
|
|
|
502
531
|
name: column.name,
|
|
503
532
|
label: column.name,
|
|
504
533
|
badges: getColumnBadges(column),
|
|
534
|
+
...getColumnNumberInputMeta(column),
|
|
505
535
|
value: value === null || value === undefined ? '' : String(value),
|
|
506
536
|
};
|
|
507
537
|
}),
|
|
@@ -265,6 +265,7 @@ export function renderEditorView(state, { isResultsRoute = false } = {}) {
|
|
|
265
265
|
error: state.editor.historyError,
|
|
266
266
|
activeTab: state.editor.historyTab,
|
|
267
267
|
search: state.editor.historySearchInput,
|
|
268
|
+
committedSearch: state.editor.historySearch,
|
|
268
269
|
total: state.editor.historyTotal,
|
|
269
270
|
hasMore: state.editor.historyHasMore,
|
|
270
271
|
activeHistoryId: state.editor.historyActiveId,
|
package/frontend/styles/base.css
CHANGED
|
@@ -28,6 +28,26 @@ button {
|
|
|
28
28
|
cursor: pointer;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
button:disabled,
|
|
32
|
+
button[aria-disabled='true'],
|
|
33
|
+
input:disabled,
|
|
34
|
+
select:disabled,
|
|
35
|
+
textarea:disabled {
|
|
36
|
+
cursor: not-allowed;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:where(a, button, input, select, textarea, [tabindex]:not([tabindex='-1'])):focus-visible {
|
|
40
|
+
outline: 2px solid var(--color-primary-container);
|
|
41
|
+
outline-offset: 2px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.control-shell:focus-within,
|
|
45
|
+
.standard-checkbox:focus-within,
|
|
46
|
+
.sql-highlight-shell:focus-within {
|
|
47
|
+
border-color: var(--primary-alpha-35);
|
|
48
|
+
box-shadow: var(--focus-ring-inset);
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
.control-button {
|
|
32
52
|
align-items: center;
|
|
33
53
|
box-sizing: border-box;
|
|
@@ -52,22 +72,22 @@ button {
|
|
|
52
72
|
|
|
53
73
|
input.control-input--ghost {
|
|
54
74
|
appearance: none;
|
|
55
|
-
background: transparent
|
|
56
|
-
background-color: transparent
|
|
57
|
-
background-image: none
|
|
58
|
-
border: 0
|
|
59
|
-
border-color: transparent
|
|
60
|
-
box-shadow: none
|
|
75
|
+
background: transparent;
|
|
76
|
+
background-color: transparent;
|
|
77
|
+
background-image: none;
|
|
78
|
+
border: 0;
|
|
79
|
+
border-color: transparent;
|
|
80
|
+
box-shadow: none;
|
|
61
81
|
-webkit-appearance: none;
|
|
62
82
|
padding: 0;
|
|
63
83
|
}
|
|
64
84
|
|
|
65
85
|
input.control-input--ghost:focus,
|
|
66
86
|
input.control-input--ghost:not(:focus) {
|
|
67
|
-
background: transparent
|
|
68
|
-
background-color: transparent
|
|
69
|
-
border-color: transparent
|
|
70
|
-
box-shadow: none
|
|
87
|
+
background: transparent;
|
|
88
|
+
background-color: transparent;
|
|
89
|
+
border-color: transparent;
|
|
90
|
+
box-shadow: none;
|
|
71
91
|
outline: none;
|
|
72
92
|
}
|
|
73
93
|
|
|
@@ -105,6 +125,7 @@ img {
|
|
|
105
125
|
}
|
|
106
126
|
|
|
107
127
|
[hidden] {
|
|
128
|
+
/* Hidden must beat utility display classes generated by Tailwind CDN. */
|
|
108
129
|
display: none !important;
|
|
109
130
|
}
|
|
110
131
|
|
|
@@ -118,9 +139,6 @@ img {
|
|
|
118
139
|
|
|
119
140
|
.font-mono {
|
|
120
141
|
font-family: var(--font-family-mono);
|
|
121
|
-
overflow: hidden;
|
|
122
|
-
white-space: nowrap;
|
|
123
|
-
text-overflow: ellipsis;
|
|
124
142
|
}
|
|
125
143
|
|
|
126
144
|
.material-symbols-outlined {
|
|
@@ -218,12 +236,14 @@ img {
|
|
|
218
236
|
width: 100%;
|
|
219
237
|
word-break: normal;
|
|
220
238
|
overflow-wrap: anywhere;
|
|
239
|
+
font-variant-ligatures: none;
|
|
221
240
|
}
|
|
222
241
|
|
|
223
242
|
.query-editor-highlight {
|
|
224
243
|
color: var(--color-on-surface);
|
|
225
244
|
overflow: visible;
|
|
226
245
|
pointer-events: none;
|
|
246
|
+
user-select: none;
|
|
227
247
|
}
|
|
228
248
|
|
|
229
249
|
.query-editor-input {
|
|
@@ -236,6 +256,14 @@ img {
|
|
|
236
256
|
overflow: auto;
|
|
237
257
|
}
|
|
238
258
|
|
|
259
|
+
.query-editor-input:focus-visible {
|
|
260
|
+
outline: none;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.query-editor-layer:focus-within {
|
|
264
|
+
box-shadow: inset 0 0 0 1px var(--primary-alpha-30);
|
|
265
|
+
}
|
|
266
|
+
|
|
239
267
|
.query-editor-gutter-track {
|
|
240
268
|
min-height: 100%;
|
|
241
269
|
will-change: transform;
|
|
@@ -260,3 +288,19 @@ img {
|
|
|
260
288
|
border-color: var(--color-primary-container);
|
|
261
289
|
}
|
|
262
290
|
}
|
|
291
|
+
|
|
292
|
+
@media (prefers-reduced-motion: reduce) {
|
|
293
|
+
*,
|
|
294
|
+
*::before,
|
|
295
|
+
*::after {
|
|
296
|
+
animation-duration: 0.001ms;
|
|
297
|
+
animation-iteration-count: 1;
|
|
298
|
+
scroll-behavior: auto;
|
|
299
|
+
transition-duration: 0.001ms;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.cursor-blink {
|
|
303
|
+
animation: none;
|
|
304
|
+
border-right-color: var(--color-primary-container);
|
|
305
|
+
}
|
|
306
|
+
}
|