sqlite-hub 1.0.0 → 1.1.1
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 +47 -33
- package/bin/sqlite-hub.js +127 -47
- package/frontend/js/api.js +6 -0
- package/frontend/js/app.js +229 -44
- package/frontend/js/components/modal.js +17 -1
- package/frontend/js/components/queryChartRenderer.js +28 -3
- package/frontend/js/components/queryEditor.js +24 -26
- package/frontend/js/components/queryHistoryDetail.js +1 -1
- package/frontend/js/components/queryHistoryHeader.js +48 -0
- package/frontend/js/components/queryHistoryList.js +132 -0
- package/frontend/js/components/queryHistoryPanel.js +72 -136
- package/frontend/js/components/structureGraph.js +700 -89
- package/frontend/js/components/tableDesignerEditor.js +26 -47
- package/frontend/js/components/tableDesignerSidebar.js +7 -31
- package/frontend/js/components/tableDesignerSqlPreview.js +2 -1
- package/frontend/js/store.js +97 -8
- package/frontend/js/utils/exportFilenames.js +3 -1
- package/frontend/js/views/charts.js +326 -174
- package/frontend/js/views/connections.js +59 -64
- package/frontend/js/views/data.js +48 -54
- package/frontend/js/views/documents.js +62 -23
- package/frontend/js/views/editor.js +0 -2
- package/frontend/js/views/mediaTagging.js +6 -5
- package/frontend/js/views/settings.js +78 -0
- package/frontend/js/views/structure.js +48 -32
- package/frontend/js/views/tableDesigner.js +45 -1
- package/frontend/styles/components.css +259 -54
- package/frontend/styles/structure-graph.css +150 -37
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +2 -0
- package/frontend/styles/views.css +75 -38
- package/package.json +3 -2
- package/server/routes/export.js +89 -22
- package/server/routes/externalApi.js +84 -2
- package/server/routes/settings.js +37 -28
- package/server/services/appInfoService.js +215 -0
- package/server/services/databaseCommandService.js +50 -6
- package/server/services/sqlite/dataBrowserService.js +11 -3
- package/server/services/sqlite/exportService.js +307 -22
- package/tests/api-token-auth.test.js +110 -1
- package/tests/cli-args.test.js +16 -3
- package/tests/database-command-service.test.js +40 -3
- package/tests/export-blob.test.js +54 -1
- package/tests/export-filenames.test.js +4 -0
- package/tests/settings-api-tokens-route.test.js +22 -1
- package/tests/settings-metadata.test.js +99 -1
- package/tests/settings-view.test.js +28 -0
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { renderQueryResultsPane } from '../components/queryResults.js';
|
|
2
2
|
import { analyzeQueryChartResult, getQueryChartTypeLabel, validateQueryChartConfig } from '../lib/queryCharts.js';
|
|
3
|
-
import { escapeHtml, highlightSql } from '../utils/format.js';
|
|
3
|
+
import { escapeHtml, formatNumber, highlightSql } from '../utils/format.js';
|
|
4
4
|
import { renderStatusBadge } from '../components/badges.js';
|
|
5
|
+
import { renderQueryHistoryHeader, renderQueryHistorySearch } from '../components/queryHistoryHeader.js';
|
|
6
|
+
import {
|
|
7
|
+
renderQueryHistoryActionGroup,
|
|
8
|
+
renderQueryHistoryBadgeRow,
|
|
9
|
+
renderQueryHistoryIconButton,
|
|
10
|
+
renderQueryHistoryListItem,
|
|
11
|
+
renderQueryHistoryTabs,
|
|
12
|
+
} from '../components/queryHistoryList.js';
|
|
5
13
|
|
|
6
14
|
function renderMissingDatabase() {
|
|
7
15
|
return `
|
|
@@ -19,11 +27,101 @@ function renderMissingDatabase() {
|
|
|
19
27
|
`;
|
|
20
28
|
}
|
|
21
29
|
|
|
22
|
-
function
|
|
23
|
-
|
|
30
|
+
function normalizeChartsHistorySearchText(value) {
|
|
31
|
+
return String(value ?? '')
|
|
32
|
+
.trim()
|
|
33
|
+
.toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getChartsHistorySearchHaystack(item) {
|
|
37
|
+
return [
|
|
38
|
+
item.displayTitle,
|
|
39
|
+
item.title,
|
|
40
|
+
item.previewSql,
|
|
41
|
+
item.rawSql,
|
|
42
|
+
item.chartTypes?.join(' '),
|
|
43
|
+
item.isSaved ? 'saved' : '',
|
|
44
|
+
]
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(' ')
|
|
47
|
+
.toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getFilteredChartsQueries(state) {
|
|
51
|
+
const activeTab = ['recent', 'saved', 'unsaved'].includes(state.charts.historyTab)
|
|
52
|
+
? state.charts.historyTab
|
|
53
|
+
: 'recent';
|
|
54
|
+
const search = normalizeChartsHistorySearchText(state.charts.historySearch);
|
|
24
55
|
const allQueries = state.charts.queries ?? [];
|
|
25
|
-
const
|
|
56
|
+
const tabQueries =
|
|
57
|
+
activeTab === 'saved'
|
|
58
|
+
? allQueries.filter(item => item.isSaved)
|
|
59
|
+
: activeTab === 'unsaved'
|
|
60
|
+
? allQueries.filter(item => !item.isSaved)
|
|
61
|
+
: allQueries;
|
|
62
|
+
|
|
63
|
+
if (!search) {
|
|
64
|
+
return tabQueries;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return tabQueries.filter(item => getChartsHistorySearchHaystack(item).includes(search));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function renderChartsHistoryListItem(item, state) {
|
|
26
71
|
const selectedHistoryId = Number(state.charts.selectedHistoryId);
|
|
72
|
+
const isActive = selectedHistoryId === Number(item.id);
|
|
73
|
+
const savedBadgeMarkup = `
|
|
74
|
+
<span
|
|
75
|
+
data-charts-saved-badge
|
|
76
|
+
data-history-id="${escapeHtml(item.id)}"
|
|
77
|
+
${item.isSaved ? '' : 'hidden'}
|
|
78
|
+
>
|
|
79
|
+
${renderStatusBadge('saved', 'primary')}
|
|
80
|
+
</span>
|
|
81
|
+
`;
|
|
82
|
+
const chartBadgesMarkup = item.chartTypes?.length
|
|
83
|
+
? item.chartTypes.map(chartType => renderStatusBadge(getQueryChartTypeLabel(chartType), 'primary')).join('')
|
|
84
|
+
: renderStatusBadge('None', 'muted');
|
|
85
|
+
const badgesMarkup = renderQueryHistoryBadgeRow([savedBadgeMarkup, chartBadgesMarkup].join(''));
|
|
86
|
+
const actionsMarkup = renderQueryHistoryActionGroup([
|
|
87
|
+
renderQueryHistoryIconButton({
|
|
88
|
+
action: 'open-charts-query-detail',
|
|
89
|
+
historyId: item.id,
|
|
90
|
+
icon: 'info',
|
|
91
|
+
title: 'Open chart detail',
|
|
92
|
+
active: Boolean(state.charts.detailPanelVisible && isActive),
|
|
93
|
+
}),
|
|
94
|
+
renderQueryHistoryIconButton({
|
|
95
|
+
action: 'toggle-charts-query-history-saved',
|
|
96
|
+
historyId: item.id,
|
|
97
|
+
icon: item.isSaved ? 'bookmark' : 'bookmark_add',
|
|
98
|
+
title: item.isSaved ? 'Remove from saved' : 'Save query',
|
|
99
|
+
active: item.isSaved,
|
|
100
|
+
nextValue: item.isSaved ? 'false' : 'true',
|
|
101
|
+
}),
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
return renderQueryHistoryListItem({
|
|
105
|
+
title: item.displayTitle,
|
|
106
|
+
preview: item.previewSql,
|
|
107
|
+
historyId: item.id,
|
|
108
|
+
active: isActive,
|
|
109
|
+
hitAction: 'navigate',
|
|
110
|
+
hitAttrs: `data-to="/charts/${encodeURIComponent(item.id)}"`,
|
|
111
|
+
itemAttrs: 'data-charts-history-item',
|
|
112
|
+
titleAttrs: 'data-charts-history-title',
|
|
113
|
+
badgesMarkup,
|
|
114
|
+
actionsMarkup,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function renderChartsList(state) {
|
|
119
|
+
const activeTab = ['recent', 'saved', 'unsaved'].includes(state.charts.historyTab)
|
|
120
|
+
? state.charts.historyTab
|
|
121
|
+
: 'recent';
|
|
122
|
+
const allQueries = state.charts.queries ?? [];
|
|
123
|
+
const queries = getFilteredChartsQueries(state);
|
|
124
|
+
const hasSearch = Boolean(normalizeChartsHistorySearchText(state.charts.historySearch));
|
|
27
125
|
|
|
28
126
|
if (state.charts.loading && !allQueries.length) {
|
|
29
127
|
return `
|
|
@@ -48,21 +146,34 @@ function renderChartsList(state) {
|
|
|
48
146
|
|
|
49
147
|
if (!queries.length) {
|
|
50
148
|
const isSavedTab = activeTab === 'saved';
|
|
149
|
+
const isUnsavedTab = activeTab === 'unsaved';
|
|
51
150
|
|
|
52
151
|
return `
|
|
53
152
|
<div class="flex h-full items-center justify-center px-6 text-center">
|
|
54
153
|
<div>
|
|
55
154
|
<span class="material-symbols-outlined mb-3 text-4xl text-on-surface-variant/25">${
|
|
56
|
-
isSavedTab ? 'bookmark' : 'query_stats'
|
|
155
|
+
hasSearch ? 'manage_search' : isSavedTab ? 'bookmark' : isUnsavedTab ? 'bookmark_border' : 'query_stats'
|
|
57
156
|
}</span>
|
|
58
157
|
<p class="font-headline text-lg font-black uppercase tracking-tight text-on-surface">
|
|
59
|
-
${
|
|
158
|
+
${
|
|
159
|
+
hasSearch
|
|
160
|
+
? 'No Matching Queries'
|
|
161
|
+
: isSavedTab
|
|
162
|
+
? 'No Saved Charts Queries'
|
|
163
|
+
: isUnsavedTab
|
|
164
|
+
? 'No Unsaved Charts Queries'
|
|
165
|
+
: 'No Chartable Queries'
|
|
166
|
+
}
|
|
60
167
|
</p>
|
|
61
168
|
<p class="mt-2 max-w-xs text-sm leading-6 text-on-surface-variant/60">
|
|
62
169
|
${
|
|
63
|
-
|
|
64
|
-
? '
|
|
65
|
-
:
|
|
170
|
+
hasSearch
|
|
171
|
+
? 'Try another title, SQL fragment, chart type, or saved status.'
|
|
172
|
+
: isSavedTab
|
|
173
|
+
? 'Save chartable queries from this list or from the SQL Editor to keep them here.'
|
|
174
|
+
: isUnsavedTab
|
|
175
|
+
? 'Unsaved chartable queries will appear here until you bookmark them.'
|
|
176
|
+
: 'Run SELECT queries in the SQL Editor first. They will appear here automatically.'
|
|
66
177
|
}
|
|
67
178
|
</p>
|
|
68
179
|
</div>
|
|
@@ -71,109 +182,37 @@ function renderChartsList(state) {
|
|
|
71
182
|
}
|
|
72
183
|
|
|
73
184
|
return `
|
|
74
|
-
<div class="
|
|
75
|
-
<div class="
|
|
76
|
-
${queries
|
|
77
|
-
.map(
|
|
78
|
-
item => `
|
|
79
|
-
<article
|
|
80
|
-
class="group w-full border transition-colors ${
|
|
81
|
-
selectedHistoryId === Number(item.id)
|
|
82
|
-
? 'border-primary-container/30 bg-surface-container-high'
|
|
83
|
-
: 'border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high'
|
|
84
|
-
}"
|
|
85
|
-
data-charts-history-item
|
|
86
|
-
>
|
|
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>
|
|
112
|
-
</div>
|
|
113
|
-
<div class="mt-1 truncate text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">
|
|
114
|
-
${escapeHtml(item.previewSql)}
|
|
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>
|
|
138
|
-
</div>
|
|
139
|
-
</article>
|
|
140
|
-
`,
|
|
141
|
-
)
|
|
142
|
-
.join('')}
|
|
185
|
+
<div class="query-history-list-scroll custom-scrollbar">
|
|
186
|
+
<div class="query-history-list">
|
|
187
|
+
${queries.map(item => renderChartsHistoryListItem(item, state)).join('')}
|
|
143
188
|
</div>
|
|
144
189
|
</div>
|
|
145
190
|
`;
|
|
146
191
|
}
|
|
147
192
|
|
|
148
193
|
function renderChartsHistoryTabs(state) {
|
|
149
|
-
const activeTab = ['recent', 'saved'].includes(state.charts.historyTab)
|
|
150
|
-
|
|
194
|
+
const activeTab = ['recent', 'saved', 'unsaved'].includes(state.charts.historyTab)
|
|
195
|
+
? state.charts.historyTab
|
|
196
|
+
: 'recent';
|
|
197
|
+
const search = normalizeChartsHistorySearchText(state.charts.historySearch);
|
|
198
|
+
const countableQueries = search
|
|
199
|
+
? (state.charts.queries ?? []).filter(item => getChartsHistorySearchHaystack(item).includes(search))
|
|
200
|
+
: (state.charts.queries ?? []);
|
|
201
|
+
const savedCount = countableQueries.filter(item => item.isSaved).length;
|
|
202
|
+
const unsavedCount = countableQueries.filter(item => !item.isSaved).length;
|
|
151
203
|
const tabs = [
|
|
152
|
-
{ id: 'recent', label: 'Recent', count:
|
|
204
|
+
{ id: 'recent', label: 'Recent', count: countableQueries.length },
|
|
153
205
|
{ id: 'saved', label: 'Saved', count: savedCount },
|
|
206
|
+
{ id: 'unsaved', label: 'Unsaved', count: unsavedCount },
|
|
154
207
|
];
|
|
155
208
|
|
|
156
|
-
return
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
`;
|
|
209
|
+
return renderQueryHistoryTabs({
|
|
210
|
+
tabs,
|
|
211
|
+
activeTab,
|
|
212
|
+
action: 'set-charts-history-tab',
|
|
213
|
+
count: tabs.find(tab => tab.id === activeTab)?.count ?? 0,
|
|
214
|
+
countAttr: 'data-charts-history-count',
|
|
215
|
+
});
|
|
177
216
|
}
|
|
178
217
|
|
|
179
218
|
function renderEmptyChartDetail() {
|
|
@@ -185,7 +224,7 @@ function renderEmptyChartDetail() {
|
|
|
185
224
|
Select A Query
|
|
186
225
|
</h2>
|
|
187
226
|
<p class="mt-3 text-sm leading-7 text-on-surface-variant/65">
|
|
188
|
-
Choose a query-history entry on the
|
|
227
|
+
Choose a query-history entry on the right to load its charts, render the live result set, and manage chart definitions.
|
|
189
228
|
</p>
|
|
190
229
|
</div>
|
|
191
230
|
</div>
|
|
@@ -231,60 +270,45 @@ function renderQueryResultState(state, result) {
|
|
|
231
270
|
return '';
|
|
232
271
|
}
|
|
233
272
|
|
|
234
|
-
function
|
|
235
|
-
|
|
273
|
+
function renderChartsDetailDrawerMetaItem(label, value) {
|
|
274
|
+
return `
|
|
275
|
+
<div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
|
|
276
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
277
|
+
${escapeHtml(label)}
|
|
278
|
+
</div>
|
|
279
|
+
<div class="mt-2 text-sm text-on-surface">${escapeHtml(value)}</div>
|
|
280
|
+
</div>
|
|
281
|
+
`;
|
|
282
|
+
}
|
|
236
283
|
|
|
284
|
+
function renderChartsDetailDrawerQuerySection(rawSql) {
|
|
237
285
|
return `
|
|
238
286
|
<section class="mt-5 border border-outline-variant/10 bg-surface-container-lowest">
|
|
239
|
-
<
|
|
240
|
-
class="standard-button flex w-full justify-between border-b border-outline-variant/10 px-4 text-left"
|
|
241
|
-
data-action="toggle-charts-sql-panel"
|
|
242
|
-
type="button"
|
|
243
|
-
>
|
|
287
|
+
<div class="border-b border-outline-variant/10 px-4 py-3">
|
|
244
288
|
<span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
245
|
-
Query
|
|
289
|
+
Query
|
|
246
290
|
</span>
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
</
|
|
251
|
-
${
|
|
252
|
-
isExpanded
|
|
253
|
-
? `
|
|
254
|
-
<pre class="custom-scrollbar overflow-auto p-4 font-mono text-sm leading-6 text-on-surface"><code>${highlightSql(
|
|
255
|
-
rawSql,
|
|
256
|
-
)}</code></pre>
|
|
257
|
-
`
|
|
258
|
-
: ''
|
|
259
|
-
}
|
|
291
|
+
</div>
|
|
292
|
+
<pre class="query-history-detail-sql custom-scrollbar max-h-[22rem] overflow-auto p-4 font-mono text-sm leading-6 text-on-surface"><code>${highlightSql(
|
|
293
|
+
rawSql ?? '',
|
|
294
|
+
)}</code></pre>
|
|
260
295
|
</section>
|
|
261
296
|
`;
|
|
262
297
|
}
|
|
263
298
|
|
|
264
|
-
function
|
|
265
|
-
const isVisible = Boolean(state.charts.resultsVisible);
|
|
299
|
+
function renderChartsDetailDrawerResultsSection(state) {
|
|
266
300
|
const statusMarkup = renderQueryResultState(state, state.charts.result);
|
|
267
301
|
|
|
268
302
|
return `
|
|
269
|
-
<section class="mt-
|
|
270
|
-
<div class="
|
|
303
|
+
<section class="mt-5 border border-outline-variant/10 bg-surface-container-lowest">
|
|
304
|
+
<div class="border-b border-outline-variant/10 px-4 py-3">
|
|
271
305
|
<span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
272
306
|
Results
|
|
273
307
|
</span>
|
|
274
|
-
<button
|
|
275
|
-
class="standard-button"
|
|
276
|
-
data-action="toggle-charts-results-panel"
|
|
277
|
-
type="button"
|
|
278
|
-
>
|
|
279
|
-
<span class="material-symbols-outlined text-sm">
|
|
280
|
-
${isVisible ? 'table_rows_narrow' : 'table_rows'}
|
|
281
|
-
</span>
|
|
282
|
-
${isVisible ? 'Hide Results' : 'Show Results'}
|
|
283
|
-
</button>
|
|
284
308
|
</div>
|
|
285
309
|
${statusMarkup}
|
|
286
310
|
${
|
|
287
|
-
|
|
311
|
+
state.charts.result && !state.charts.resultLoading && !state.charts.resultError
|
|
288
312
|
? `
|
|
289
313
|
<div class="h-[18rem] overflow-hidden">
|
|
290
314
|
${renderQueryResultsPane(state.charts.result, {
|
|
@@ -303,6 +327,140 @@ function renderQueryResultsSection(state) {
|
|
|
303
327
|
`;
|
|
304
328
|
}
|
|
305
329
|
|
|
330
|
+
export function renderChartsDetailDrawer(state) {
|
|
331
|
+
if (!state.charts.detailPanelVisible) {
|
|
332
|
+
return '';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const detail = state.charts.detail;
|
|
336
|
+
|
|
337
|
+
if (state.charts.detailLoading && !detail) {
|
|
338
|
+
return `
|
|
339
|
+
<section class="flex h-full min-h-0 flex-col bg-surface-low">
|
|
340
|
+
<div class="border-b border-outline-variant/10 px-5 py-4">
|
|
341
|
+
<div class="flex items-center justify-between gap-3">
|
|
342
|
+
<span class="font-headline text-sm font-black uppercase tracking-[0.18em] text-primary-container">
|
|
343
|
+
Chart Detail
|
|
344
|
+
</span>
|
|
345
|
+
<button class="query-history-icon-button" data-action="close-charts-query-detail" type="button">
|
|
346
|
+
<span class="material-symbols-outlined text-[18px]">close</span>
|
|
347
|
+
</button>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
<div class="flex flex-1 items-center justify-center px-6 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
351
|
+
Loading chart detail...
|
|
352
|
+
</div>
|
|
353
|
+
</section>
|
|
354
|
+
`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (state.charts.detailError && !detail) {
|
|
358
|
+
return `
|
|
359
|
+
<section class="flex h-full min-h-0 flex-col bg-surface-low">
|
|
360
|
+
<div class="border-b border-outline-variant/10 px-5 py-4">
|
|
361
|
+
<div class="flex items-center justify-between gap-3">
|
|
362
|
+
<span class="font-headline text-sm font-black uppercase tracking-[0.18em] text-primary-container">
|
|
363
|
+
Chart Detail
|
|
364
|
+
</span>
|
|
365
|
+
<button class="query-history-icon-button" data-action="close-charts-query-detail" type="button">
|
|
366
|
+
<span class="material-symbols-outlined text-[18px]">close</span>
|
|
367
|
+
</button>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
<div class="p-5">
|
|
371
|
+
<div class="border border-error/30 bg-error-container/20 px-4 py-4 text-sm text-error">
|
|
372
|
+
${escapeHtml(state.charts.detailError.message)}
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</section>
|
|
376
|
+
`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!detail?.item) {
|
|
380
|
+
return '';
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const charts = detail.charts ?? [];
|
|
384
|
+
const result = state.charts.result;
|
|
385
|
+
const chartTypeBadges = charts.length
|
|
386
|
+
? charts.map(chart => renderStatusBadge(getQueryChartTypeLabel(chart.chartType), 'primary')).join('')
|
|
387
|
+
: renderStatusBadge('no charts', 'muted');
|
|
388
|
+
const savedBadgeMarkup = `
|
|
389
|
+
<span
|
|
390
|
+
data-charts-saved-badge
|
|
391
|
+
data-history-id="${escapeHtml(detail.item.id)}"
|
|
392
|
+
${detail.item.isSaved ? '' : 'hidden'}
|
|
393
|
+
>
|
|
394
|
+
${renderStatusBadge('saved', 'primary')}
|
|
395
|
+
</span>
|
|
396
|
+
`;
|
|
397
|
+
const statusMarkup = [
|
|
398
|
+
savedBadgeMarkup,
|
|
399
|
+
state.charts.resultError ? renderStatusBadge('result error', 'alert') : '',
|
|
400
|
+
state.charts.resultLoading ? renderStatusBadge('loading result', 'muted') : '',
|
|
401
|
+
chartTypeBadges,
|
|
402
|
+
].join('');
|
|
403
|
+
const metaMarkup = [
|
|
404
|
+
renderChartsDetailDrawerMetaItem('Charts', formatNumber(charts.length)),
|
|
405
|
+
renderChartsDetailDrawerMetaItem('Rows', formatNumber(result?.rows?.length ?? 0)),
|
|
406
|
+
renderChartsDetailDrawerMetaItem('Columns', formatNumber(result?.columns?.length ?? 0)),
|
|
407
|
+
renderChartsDetailDrawerMetaItem('Query ID', String(detail.item.id)),
|
|
408
|
+
].join('');
|
|
409
|
+
|
|
410
|
+
return `
|
|
411
|
+
<section class="flex h-full min-h-0 flex-col bg-surface-low">
|
|
412
|
+
<div class="border-b border-outline-variant/10 px-5 py-4">
|
|
413
|
+
<div class="flex items-center justify-between gap-3">
|
|
414
|
+
<div class="min-w-0">
|
|
415
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/70">
|
|
416
|
+
Chart Detail
|
|
417
|
+
</div>
|
|
418
|
+
<h2 class="mt-1 truncate font-headline text-lg font-black uppercase tracking-tight text-on-surface">
|
|
419
|
+
${escapeHtml(detail.item.displayTitle)}
|
|
420
|
+
</h2>
|
|
421
|
+
</div>
|
|
422
|
+
<button
|
|
423
|
+
class="query-history-icon-button"
|
|
424
|
+
data-action="close-charts-query-detail"
|
|
425
|
+
title="Close chart detail"
|
|
426
|
+
type="button"
|
|
427
|
+
>
|
|
428
|
+
<span class="material-symbols-outlined text-[18px]">close</span>
|
|
429
|
+
</button>
|
|
430
|
+
</div>
|
|
431
|
+
${renderQueryHistoryBadgeRow(statusMarkup, { compact: false, className: 'mt-4' })}
|
|
432
|
+
</div>
|
|
433
|
+
<div class="custom-scrollbar min-h-0 flex-1 overflow-auto px-5 py-5">
|
|
434
|
+
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
435
|
+
${metaMarkup}
|
|
436
|
+
</div>
|
|
437
|
+
<div class="mt-5 flex flex-wrap gap-2">
|
|
438
|
+
<button
|
|
439
|
+
class="standard-button"
|
|
440
|
+
data-action="open-query-history"
|
|
441
|
+
data-history-id="${escapeHtml(detail.item.id)}"
|
|
442
|
+
type="button"
|
|
443
|
+
>
|
|
444
|
+
Open In Editor
|
|
445
|
+
</button>
|
|
446
|
+
<button
|
|
447
|
+
class="standard-button"
|
|
448
|
+
data-action="toggle-charts-query-history-saved"
|
|
449
|
+
data-history-id="${escapeHtml(detail.item.id)}"
|
|
450
|
+
data-next-value="${detail.item.isSaved ? 'false' : 'true'}"
|
|
451
|
+
title="${detail.item.isSaved ? 'Remove from saved' : 'Save query'}"
|
|
452
|
+
type="button"
|
|
453
|
+
>
|
|
454
|
+
<span data-charts-saved-label>${detail.item.isSaved ? 'Unsave' : 'Save'}</span>
|
|
455
|
+
</button>
|
|
456
|
+
</div>
|
|
457
|
+
${renderChartsDetailDrawerQuerySection(detail.item.rawSql)}
|
|
458
|
+
${renderChartsDetailDrawerResultsSection(state)}
|
|
459
|
+
</div>
|
|
460
|
+
</section>
|
|
461
|
+
`;
|
|
462
|
+
}
|
|
463
|
+
|
|
306
464
|
function resolveChartCardSizeClass(state) {
|
|
307
465
|
return `query-chart-card--${String(state.charts.chartHeightPreset ?? 'medium')
|
|
308
466
|
.trim()
|
|
@@ -391,7 +549,7 @@ function renderChartCard(chart, state, analysis) {
|
|
|
391
549
|
'<div class="flex flex-wrap items-center gap-2">',
|
|
392
550
|
'<h3 class="truncate font-headline text-xl font-black uppercase tracking-tight text-on-surface">',
|
|
393
551
|
escapeHtml(chart.name),
|
|
394
|
-
|
|
552
|
+
'</h3>',
|
|
395
553
|
renderStatusBadge(getQueryChartTypeLabel(chart.chartType), 'primary'),
|
|
396
554
|
'</div></div><div class="flex flex-wrap items-center gap-2">',
|
|
397
555
|
'<button class="standard-button" data-action="open-edit-query-chart-modal" data-chart-id="',
|
|
@@ -405,8 +563,8 @@ function renderChartCard(chart, state, analysis) {
|
|
|
405
563
|
'" type="button">Export PNG</button>',
|
|
406
564
|
'</div></header><div class="query-chart-card__body">',
|
|
407
565
|
renderChartSurface(chart, state, analysis),
|
|
408
|
-
|
|
409
|
-
].join(
|
|
566
|
+
'</div></article>',
|
|
567
|
+
].join('');
|
|
410
568
|
}
|
|
411
569
|
|
|
412
570
|
export function renderChartsDetail(state) {
|
|
@@ -447,12 +605,9 @@ export function renderChartsDetail(state) {
|
|
|
447
605
|
const charts = detail.charts ?? [];
|
|
448
606
|
|
|
449
607
|
return `
|
|
450
|
-
<div class="custom-scrollbar flex-1 overflow-auto">
|
|
608
|
+
<div class="charts-detail-scroll custom-scrollbar flex-1 overflow-auto">
|
|
451
609
|
<div class="charts-detail-shell">
|
|
452
|
-
<header class="charts-detail-shell__header">
|
|
453
|
-
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/70">
|
|
454
|
-
Charts
|
|
455
|
-
</div>
|
|
610
|
+
<header class="charts-detail-shell__header" data-charts-detail-header>
|
|
456
611
|
<div class="charts-detail-shell__title">
|
|
457
612
|
<h1 class="mt-2 truncate font-headline text-4xl font-black uppercase tracking-tight text-primary-container">
|
|
458
613
|
${escapeHtml(detail.item.displayTitle)}
|
|
@@ -461,19 +616,6 @@ export function renderChartsDetail(state) {
|
|
|
461
616
|
<div class="charts-detail-shell__controls">
|
|
462
617
|
<div class="charts-detail-shell__controls-group">
|
|
463
618
|
${renderChartHeightPresetToggle(state)}
|
|
464
|
-
<button
|
|
465
|
-
class="standard-button"
|
|
466
|
-
data-action="toggle-query-history-panel"
|
|
467
|
-
data-next-value="${historyVisible ? 'false' : 'true'}"
|
|
468
|
-
type="button"
|
|
469
|
-
>
|
|
470
|
-
<span class="material-symbols-outlined text-sm">${
|
|
471
|
-
historyVisible ? 'visibility_off' : 'visibility'
|
|
472
|
-
}</span>
|
|
473
|
-
${historyVisible ? 'Hide Query History' : 'Show Query History'}
|
|
474
|
-
</button>
|
|
475
|
-
</div>
|
|
476
|
-
<div class="charts-detail-shell__controls-group charts-detail-shell__controls-group--end">
|
|
477
619
|
<button
|
|
478
620
|
class="standard-button"
|
|
479
621
|
data-action="open-query-history"
|
|
@@ -492,12 +634,24 @@ export function renderChartsDetail(state) {
|
|
|
492
634
|
New Chart
|
|
493
635
|
</button>
|
|
494
636
|
</div>
|
|
637
|
+
<div class="charts-detail-shell__controls-group charts-detail-shell__controls-group--end">
|
|
638
|
+
<button
|
|
639
|
+
class="standard-button panel-toggle-button ${historyVisible ? '' : 'is-active'}"
|
|
640
|
+
aria-pressed="${historyVisible ? 'false' : 'true'}"
|
|
641
|
+
data-action="toggle-query-history-panel"
|
|
642
|
+
data-next-value="${historyVisible ? 'false' : 'true'}"
|
|
643
|
+
type="button"
|
|
644
|
+
>
|
|
645
|
+
<span class="material-symbols-outlined text-sm">${
|
|
646
|
+
historyVisible ? 'visibility_off' : 'visibility'
|
|
647
|
+
}</span>
|
|
648
|
+
${historyVisible ? 'Hide History' : 'Show History'}
|
|
649
|
+
</button>
|
|
650
|
+
</div>
|
|
495
651
|
</div>
|
|
496
652
|
</header>
|
|
497
653
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
<section class="mt-6 space-y-5">
|
|
654
|
+
<section class="mt-6 space-y-5" data-charts-card-list>
|
|
501
655
|
${
|
|
502
656
|
charts.length
|
|
503
657
|
? charts.map(chart => renderChartCard(chart, state, analysis)).join('')
|
|
@@ -517,7 +671,6 @@ export function renderChartsDetail(state) {
|
|
|
517
671
|
}
|
|
518
672
|
</section>
|
|
519
673
|
|
|
520
|
-
${renderQueryResultsSection(state)}
|
|
521
674
|
</div>
|
|
522
675
|
</div>
|
|
523
676
|
`;
|
|
@@ -536,27 +689,26 @@ export function renderChartsView(state) {
|
|
|
536
689
|
return {
|
|
537
690
|
main: `
|
|
538
691
|
<section class="charts-view">
|
|
692
|
+
<div class="charts-view__detail">${renderChartsDetail(state)}</div>
|
|
539
693
|
${
|
|
540
694
|
historyVisible
|
|
541
695
|
? `
|
|
542
|
-
<aside class="charts-view__sidebar">
|
|
543
|
-
<div class="charts-view__sidebar-header">
|
|
544
|
-
|
|
545
|
-
Query History
|
|
546
|
-
</div>
|
|
547
|
-
<h2 class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
548
|
-
Charts
|
|
549
|
-
</h2>
|
|
696
|
+
<aside class="query-history-panel charts-view__sidebar">
|
|
697
|
+
<div class="query-history-panel__header charts-view__sidebar-header">
|
|
698
|
+
${renderQueryHistoryHeader()}
|
|
550
699
|
${renderChartsHistoryTabs(state)}
|
|
700
|
+
${renderQueryHistorySearch({
|
|
701
|
+
bind: 'charts-history-search',
|
|
702
|
+
value: state.charts.historySearchInput,
|
|
703
|
+
})}
|
|
551
704
|
</div>
|
|
552
705
|
${renderChartsList(state)}
|
|
553
706
|
</aside>
|
|
554
707
|
`
|
|
555
708
|
: ''
|
|
556
709
|
}
|
|
557
|
-
<div class="charts-view__detail">${renderChartsDetail(state)}</div>
|
|
558
710
|
</section>
|
|
559
711
|
`,
|
|
560
|
-
panel:
|
|
712
|
+
panel: renderChartsDetailDrawer(state),
|
|
561
713
|
};
|
|
562
714
|
}
|