sqlite-hub 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -2
- package/bin/sqlite-hub.js +285 -4
- package/changelog.md +15 -0
- package/frontend/assets/mockups/connections.png +0 -0
- package/frontend/assets/mockups/data.png +0 -0
- package/frontend/assets/mockups/data_row_editor.png +0 -0
- package/frontend/assets/mockups/home.png +0 -0
- package/frontend/assets/mockups/sql_editor.png +0 -0
- package/frontend/assets/mockups/sql_editor_croped.png +0 -0
- package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
- package/frontend/assets/mockups/structure.png +0 -0
- package/frontend/assets/mockups/structure_inspector.png +0 -0
- package/frontend/index.html +1 -0
- package/frontend/js/api.js +49 -0
- package/frontend/js/app.js +377 -9
- package/frontend/js/components/modal.js +314 -1
- package/frontend/js/components/queryChartRenderer.js +125 -0
- package/frontend/js/components/queryEditor.js +37 -8
- package/frontend/js/components/queryHistoryPanel.js +16 -5
- package/frontend/js/components/sidebar.js +2 -0
- package/frontend/js/components/tableDesignerEditor.js +356 -0
- package/frontend/js/components/tableDesignerSidebar.js +129 -0
- package/frontend/js/components/tableDesignerSqlPreview.js +40 -0
- package/frontend/js/lib/queryChartOptions.js +283 -0
- package/frontend/js/lib/queryCharts.js +560 -0
- package/frontend/js/router.js +18 -0
- package/frontend/js/store.js +914 -1
- package/frontend/js/utils/tableDesigner.js +1192 -0
- package/frontend/js/views/charts.js +471 -0
- package/frontend/js/views/data.js +281 -277
- package/frontend/js/views/editor.js +19 -13
- package/frontend/js/views/structure.js +81 -39
- package/frontend/js/views/tableDesigner.js +37 -0
- package/frontend/styles/base.css +87 -73
- package/frontend/styles/components.css +790 -251
- package/frontend/styles/views.css +316 -46
- package/package.json +2 -1
- package/server/routes/charts.js +153 -0
- package/server/routes/tableDesigner.js +60 -0
- package/server/server.js +10 -0
- package/server/services/sqlite/tableDesigner/changeAnalysis.js +295 -0
- package/server/services/sqlite/tableDesigner/schemaMapping.js +233 -0
- package/server/services/sqlite/tableDesigner/sql.js +63 -0
- package/server/services/sqlite/tableDesigner/validation.js +245 -0
- package/server/services/sqlite/tableDesignerService.js +181 -0
- package/server/services/storage/appStateStore.js +400 -1
- package/server/services/storage/queryHistoryChartUtils.js +145 -0
- package/frontend/assets/mockups/data_edit.png +0 -0
- package/frontend/assets/mockups/graph_visualize.png +0 -0
- package/frontend/assets/mockups/overview.png +0 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { renderQueryResultsPane } from '../components/queryResults.js';
|
|
2
|
+
import { analyzeQueryChartResult, getQueryChartTypeLabel, validateQueryChartConfig } from '../lib/queryCharts.js';
|
|
3
|
+
import { escapeHtml, highlightSql } from '../utils/format.js';
|
|
4
|
+
import { renderStatusBadge } from '../components/badges.js';
|
|
5
|
+
|
|
6
|
+
function renderMissingDatabase() {
|
|
7
|
+
return `
|
|
8
|
+
<section class="flex flex-1 items-center justify-center px-8 text-center">
|
|
9
|
+
<div class="max-w-xl">
|
|
10
|
+
<span class="material-symbols-outlined mb-4 text-5xl text-on-surface-variant/20">database_off</span>
|
|
11
|
+
<h1 class="font-headline text-3xl font-black uppercase tracking-tight text-primary-container">
|
|
12
|
+
No Active SQLite Database
|
|
13
|
+
</h1>
|
|
14
|
+
<p class="mt-3 text-sm leading-7 text-on-surface-variant/65">
|
|
15
|
+
Open a local SQLite database first. The Charts area only works against query-history entries from the active database.
|
|
16
|
+
</p>
|
|
17
|
+
</div>
|
|
18
|
+
</section>
|
|
19
|
+
`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function renderChartsList(state) {
|
|
23
|
+
const queries = state.charts.queries ?? [];
|
|
24
|
+
const selectedHistoryId = Number(state.charts.selectedHistoryId);
|
|
25
|
+
|
|
26
|
+
if (state.charts.loading && !queries.length) {
|
|
27
|
+
return `
|
|
28
|
+
<div class="flex h-full items-center justify-center px-6 text-center text-on-surface-variant/45">
|
|
29
|
+
<div>
|
|
30
|
+
<span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
|
|
31
|
+
<p class="font-mono text-[10px] uppercase tracking-[0.18em]">Loading Queries</p>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (state.charts.error && !queries.length) {
|
|
38
|
+
return `
|
|
39
|
+
<div class="p-5">
|
|
40
|
+
<div class="border border-error/30 bg-error-container/20 px-4 py-4 text-sm text-error">
|
|
41
|
+
${escapeHtml(state.charts.error.message)}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!queries.length) {
|
|
48
|
+
return `
|
|
49
|
+
<div class="flex h-full items-center justify-center px-6 text-center">
|
|
50
|
+
<div>
|
|
51
|
+
<span class="material-symbols-outlined mb-3 text-4xl text-on-surface-variant/25">query_stats</span>
|
|
52
|
+
<p class="font-headline text-lg font-black uppercase tracking-tight text-on-surface">
|
|
53
|
+
No Query History
|
|
54
|
+
</p>
|
|
55
|
+
<p class="mt-2 max-w-xs text-sm leading-6 text-on-surface-variant/60">
|
|
56
|
+
Run saved queries in the SQL Editor first. They will appear here automatically.
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return `
|
|
64
|
+
<div class="custom-scrollbar flex-1 overflow-auto px-4 py-4">
|
|
65
|
+
<div class="space-y-2">
|
|
66
|
+
${queries
|
|
67
|
+
.map(
|
|
68
|
+
item => `
|
|
69
|
+
<button
|
|
70
|
+
class="w-full border px-4 py-3 text-left transition-colors ${
|
|
71
|
+
selectedHistoryId === item.id
|
|
72
|
+
? 'border-primary-container/30 bg-surface-container-high'
|
|
73
|
+
: 'border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high'
|
|
74
|
+
}"
|
|
75
|
+
data-action="navigate"
|
|
76
|
+
data-to="/charts/${encodeURIComponent(item.id)}"
|
|
77
|
+
type="button"
|
|
78
|
+
>
|
|
79
|
+
<div class="flex items-start justify-between gap-3">
|
|
80
|
+
<div class="min-w-0 flex-1 truncate font-mono text-xs ${
|
|
81
|
+
selectedHistoryId === item.id ? 'text-primary-container' : 'text-on-surface'
|
|
82
|
+
}">
|
|
83
|
+
${escapeHtml(item.displayTitle)}
|
|
84
|
+
</div>
|
|
85
|
+
<div class="flex shrink-0 flex-wrap justify-end gap-1">
|
|
86
|
+
${
|
|
87
|
+
item.chartTypes?.length
|
|
88
|
+
? item.chartTypes
|
|
89
|
+
.map(chartType => renderStatusBadge(getQueryChartTypeLabel(chartType), 'primary'))
|
|
90
|
+
.join('')
|
|
91
|
+
: renderStatusBadge('None', 'muted')
|
|
92
|
+
}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="mt-1 truncate text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">
|
|
96
|
+
${escapeHtml(item.previewSql)}
|
|
97
|
+
</div>
|
|
98
|
+
</button>
|
|
99
|
+
`,
|
|
100
|
+
)
|
|
101
|
+
.join('')}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderEmptyChartDetail() {
|
|
108
|
+
return `
|
|
109
|
+
<div class="flex flex-1 items-center justify-center px-8 py-10 text-center">
|
|
110
|
+
<div class="max-w-lg">
|
|
111
|
+
<span class="material-symbols-outlined mb-4 text-5xl text-on-surface-variant/20">bar_chart</span>
|
|
112
|
+
<h2 class="font-headline text-2xl font-black uppercase tracking-tight text-primary-container">
|
|
113
|
+
Select A Query
|
|
114
|
+
</h2>
|
|
115
|
+
<p class="mt-3 text-sm leading-7 text-on-surface-variant/65">
|
|
116
|
+
Choose a query-history entry on the left to load its charts, render the live result set, and manage chart definitions.
|
|
117
|
+
</p>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderQueryResultState(state, result) {
|
|
124
|
+
if (state.charts.resultLoading && !result) {
|
|
125
|
+
return `
|
|
126
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-4 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
127
|
+
Loading live query result...
|
|
128
|
+
</div>
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (state.charts.resultError) {
|
|
133
|
+
return `
|
|
134
|
+
<div class="border border-error/30 bg-error-container/15 px-4 py-4 text-sm text-error">
|
|
135
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.18em]">
|
|
136
|
+
${escapeHtml(state.charts.resultError.code ?? 'RESULT_ERROR')}
|
|
137
|
+
</div>
|
|
138
|
+
<div class="mt-2">${escapeHtml(state.charts.resultError.message)}</div>
|
|
139
|
+
</div>
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!result) {
|
|
144
|
+
return `
|
|
145
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-4 text-sm text-on-surface-variant/60">
|
|
146
|
+
No live result set is available for this query.
|
|
147
|
+
</div>
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return '';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function renderQuerySqlSection(state, rawSql) {
|
|
155
|
+
const isExpanded = Boolean(state.charts.sqlExpanded);
|
|
156
|
+
|
|
157
|
+
return `
|
|
158
|
+
<section class="mt-5 border border-outline-variant/10 bg-surface-container-lowest">
|
|
159
|
+
<button
|
|
160
|
+
class="flex w-full items-center justify-between gap-3 border-b border-outline-variant/10 px-4 py-3 text-left"
|
|
161
|
+
data-action="toggle-charts-sql-panel"
|
|
162
|
+
type="button"
|
|
163
|
+
>
|
|
164
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
165
|
+
Query SQL
|
|
166
|
+
</span>
|
|
167
|
+
<span class="material-symbols-outlined text-on-surface-variant/55">
|
|
168
|
+
${isExpanded ? 'expand_less' : 'expand_more'}
|
|
169
|
+
</span>
|
|
170
|
+
</button>
|
|
171
|
+
${
|
|
172
|
+
isExpanded
|
|
173
|
+
? `
|
|
174
|
+
<pre class="custom-scrollbar overflow-auto p-4 font-mono text-sm leading-6 text-on-surface"><code>${highlightSql(
|
|
175
|
+
rawSql,
|
|
176
|
+
)}</code></pre>
|
|
177
|
+
`
|
|
178
|
+
: ''
|
|
179
|
+
}
|
|
180
|
+
</section>
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function renderQueryResultsSection(state) {
|
|
185
|
+
const isVisible = Boolean(state.charts.resultsVisible);
|
|
186
|
+
const statusMarkup = renderQueryResultState(state, state.charts.result);
|
|
187
|
+
|
|
188
|
+
return `
|
|
189
|
+
<section class="mt-6 border border-outline-variant/10 bg-surface-container-lowest">
|
|
190
|
+
<div class="flex items-center justify-between gap-3 border-b border-outline-variant/10 px-4 py-3">
|
|
191
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
192
|
+
Results
|
|
193
|
+
</span>
|
|
194
|
+
<button
|
|
195
|
+
class="toolbar-button query-chart-card__action"
|
|
196
|
+
data-action="toggle-charts-results-panel"
|
|
197
|
+
type="button"
|
|
198
|
+
>
|
|
199
|
+
${isVisible ? 'Hide Results' : 'Show Results'}
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
${statusMarkup}
|
|
203
|
+
${
|
|
204
|
+
isVisible && state.charts.result && !state.charts.resultLoading && !state.charts.resultError
|
|
205
|
+
? `
|
|
206
|
+
<div class="h-[18rem] overflow-hidden">
|
|
207
|
+
${renderQueryResultsPane(state.charts.result, {
|
|
208
|
+
selectedRowIndex: null,
|
|
209
|
+
editable: false,
|
|
210
|
+
sortColumn: null,
|
|
211
|
+
sortDirection: null,
|
|
212
|
+
})}
|
|
213
|
+
</div>
|
|
214
|
+
`
|
|
215
|
+
: ''
|
|
216
|
+
}
|
|
217
|
+
</section>
|
|
218
|
+
`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function resolveChartCardSizeClass(state) {
|
|
222
|
+
return `query-chart-card--${String(state.charts.chartHeightPreset ?? 'medium')
|
|
223
|
+
.trim()
|
|
224
|
+
.toLowerCase()}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function renderChartHeightPresetToggle(state) {
|
|
228
|
+
const activePreset = String(state.charts.chartHeightPreset ?? 'medium')
|
|
229
|
+
.trim()
|
|
230
|
+
.toLowerCase();
|
|
231
|
+
const presets = [
|
|
232
|
+
{ value: 'small', label: 'Small', height: '300px' },
|
|
233
|
+
{ value: 'medium', label: 'Medium', height: '450px' },
|
|
234
|
+
{ value: 'large', label: 'Large', height: '600px' },
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
return `
|
|
238
|
+
<div class="flex flex-col gap-2">
|
|
239
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
240
|
+
Chart Height
|
|
241
|
+
</div>
|
|
242
|
+
<div class="charts-height-toggle" role="group" aria-label="Chart height preset">
|
|
243
|
+
${presets
|
|
244
|
+
.map(
|
|
245
|
+
preset => `
|
|
246
|
+
<button
|
|
247
|
+
class="toolbar-button charts-height-toggle__button ${activePreset === preset.value ? 'is-active' : ''}"
|
|
248
|
+
data-action="set-charts-height-preset"
|
|
249
|
+
data-preset="${preset.value}"
|
|
250
|
+
type="button"
|
|
251
|
+
title="${preset.height}"
|
|
252
|
+
>
|
|
253
|
+
${preset.label}
|
|
254
|
+
</button>
|
|
255
|
+
`,
|
|
256
|
+
)
|
|
257
|
+
.join('')}
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function renderChartSurface(chart, state, analysis) {
|
|
264
|
+
if (state.charts.resultLoading) {
|
|
265
|
+
return `
|
|
266
|
+
<div class="query-chart-surface-state flex h-full items-center justify-center text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
267
|
+
Loading chart result...
|
|
268
|
+
</div>
|
|
269
|
+
`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (state.charts.resultError) {
|
|
273
|
+
return `
|
|
274
|
+
<div class="query-chart-surface-state flex items-center justify-center border border-error/20 bg-error-container/10 px-6 text-center text-sm text-error">
|
|
275
|
+
${escapeHtml(state.charts.resultError.message)}
|
|
276
|
+
</div>
|
|
277
|
+
`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const validation = validateQueryChartConfig(chart.chartType, chart.config, analysis);
|
|
281
|
+
|
|
282
|
+
if (!validation.valid) {
|
|
283
|
+
return `
|
|
284
|
+
<div class="query-chart-surface-state flex items-center justify-center border border-error/20 bg-error-container/10 px-6 text-center text-sm text-error">
|
|
285
|
+
${escapeHtml(validation.errors.join(' '))}
|
|
286
|
+
</div>
|
|
287
|
+
`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return `
|
|
291
|
+
<div
|
|
292
|
+
class="query-chart-canvas w-full"
|
|
293
|
+
data-query-chart-id="${escapeHtml(chart.id)}"
|
|
294
|
+
data-chart-export-name="${escapeHtml(chart.name.replace(/[^\w.-]+/g, '_'))}"
|
|
295
|
+
></div>
|
|
296
|
+
`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function renderChartCard(chart, state, analysis) {
|
|
300
|
+
const sizeClass = resolveChartCardSizeClass(state);
|
|
301
|
+
|
|
302
|
+
return `
|
|
303
|
+
<article class="query-chart-card ${sizeClass}">
|
|
304
|
+
<header class="query-chart-card__header">
|
|
305
|
+
<div class="min-w-0">
|
|
306
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
307
|
+
<h3 class="truncate font-headline text-xl font-black uppercase tracking-tight text-on-surface">
|
|
308
|
+
${escapeHtml(chart.name)}
|
|
309
|
+
</h3>
|
|
310
|
+
${renderStatusBadge(getQueryChartTypeLabel(chart.chartType), 'primary')}
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
314
|
+
<button
|
|
315
|
+
class="toolbar-button query-chart-card__action"
|
|
316
|
+
data-action="open-edit-query-chart-modal"
|
|
317
|
+
data-chart-id="${escapeHtml(chart.id)}"
|
|
318
|
+
type="button"
|
|
319
|
+
>
|
|
320
|
+
Edit
|
|
321
|
+
</button>
|
|
322
|
+
<button
|
|
323
|
+
class="toolbar-button query-chart-card__action"
|
|
324
|
+
data-action="open-delete-query-chart-modal"
|
|
325
|
+
data-chart-id="${escapeHtml(chart.id)}"
|
|
326
|
+
type="button"
|
|
327
|
+
>
|
|
328
|
+
Delete
|
|
329
|
+
</button>
|
|
330
|
+
<button
|
|
331
|
+
class="toolbar-button query-chart-card__action"
|
|
332
|
+
data-action="export-query-chart-png"
|
|
333
|
+
data-chart-id="${escapeHtml(chart.id)}"
|
|
334
|
+
type="button"
|
|
335
|
+
>
|
|
336
|
+
Export PNG
|
|
337
|
+
</button>
|
|
338
|
+
</div>
|
|
339
|
+
</header>
|
|
340
|
+
<div class="query-chart-card__body">
|
|
341
|
+
${renderChartSurface(chart, state, analysis)}
|
|
342
|
+
</div>
|
|
343
|
+
</article>
|
|
344
|
+
`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function renderChartsDetail(state) {
|
|
348
|
+
const detail = state.charts.detail;
|
|
349
|
+
const selectedHistoryId = state.charts.selectedHistoryId;
|
|
350
|
+
|
|
351
|
+
if (!selectedHistoryId) {
|
|
352
|
+
return renderEmptyChartDetail();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (state.charts.detailLoading && !detail) {
|
|
356
|
+
return `
|
|
357
|
+
<div class="flex flex-1 items-center justify-center px-8 text-center text-on-surface-variant/45">
|
|
358
|
+
<div>
|
|
359
|
+
<span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
|
|
360
|
+
<p class="font-mono text-[10px] uppercase tracking-[0.18em]">Loading Query Detail</p>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (state.charts.detailError && !detail) {
|
|
367
|
+
return `
|
|
368
|
+
<div class="p-8">
|
|
369
|
+
<div class="border border-error/30 bg-error-container/20 px-4 py-4 text-sm text-error">
|
|
370
|
+
${escapeHtml(state.charts.detailError.message)}
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (!detail?.item) {
|
|
377
|
+
return renderEmptyChartDetail();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const analysis = state.charts.result ? analyzeQueryChartResult(state.charts.result) : null;
|
|
381
|
+
const charts = detail.charts ?? [];
|
|
382
|
+
|
|
383
|
+
return `
|
|
384
|
+
<div class="custom-scrollbar flex-1 overflow-auto">
|
|
385
|
+
<div class="charts-detail-shell">
|
|
386
|
+
<header class="charts-detail-shell__header">
|
|
387
|
+
<div class="min-w-0">
|
|
388
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/70">
|
|
389
|
+
Charts
|
|
390
|
+
</div>
|
|
391
|
+
<h1 class="mt-2 truncate font-headline text-4xl font-black uppercase tracking-tight text-primary-container">
|
|
392
|
+
${escapeHtml(detail.item.displayTitle)}
|
|
393
|
+
</h1>
|
|
394
|
+
</div>
|
|
395
|
+
<div class="charts-detail-shell__controls">
|
|
396
|
+
${renderChartHeightPresetToggle(state)}
|
|
397
|
+
<button
|
|
398
|
+
class="toolbar-button charts-detail-shell__button"
|
|
399
|
+
data-action="open-query-history"
|
|
400
|
+
data-history-id="${escapeHtml(detail.item.id)}"
|
|
401
|
+
type="button"
|
|
402
|
+
>
|
|
403
|
+
Open In Editor
|
|
404
|
+
</button>
|
|
405
|
+
<button
|
|
406
|
+
class="toolbar-button charts-detail-shell__button charts-detail-shell__button--primary"
|
|
407
|
+
data-action="open-create-query-chart-modal"
|
|
408
|
+
type="button"
|
|
409
|
+
${state.charts.resultError || !state.charts.result ? 'disabled' : ''}
|
|
410
|
+
>
|
|
411
|
+
New Chart
|
|
412
|
+
</button>
|
|
413
|
+
</div>
|
|
414
|
+
</header>
|
|
415
|
+
|
|
416
|
+
${renderQuerySqlSection(state, detail.item.rawSql)}
|
|
417
|
+
|
|
418
|
+
<section class="mt-6 space-y-5">
|
|
419
|
+
${
|
|
420
|
+
charts.length
|
|
421
|
+
? charts.map(chart => renderChartCard(chart, state, analysis)).join('')
|
|
422
|
+
: `
|
|
423
|
+
<div class="flex min-h-[240px] items-center justify-center border border-dashed border-outline-variant/20 bg-surface-container-low px-8 text-center">
|
|
424
|
+
<div>
|
|
425
|
+
<span class="material-symbols-outlined mb-3 text-4xl text-on-surface-variant/25">add_chart</span>
|
|
426
|
+
<p class="font-headline text-lg font-black uppercase tracking-tight text-on-surface">
|
|
427
|
+
No Charts Yet
|
|
428
|
+
</p>
|
|
429
|
+
<p class="mt-2 max-w-md text-sm leading-6 text-on-surface-variant/60">
|
|
430
|
+
Create the first chart for this query. The query stays read-only here; only chart definitions are editable.
|
|
431
|
+
</p>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
`
|
|
435
|
+
}
|
|
436
|
+
</section>
|
|
437
|
+
|
|
438
|
+
${renderQueryResultsSection(state)}
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function renderChartsView(state) {
|
|
445
|
+
if (!state.connections.active && state.charts.error?.code === 'ACTIVE_DATABASE_REQUIRED') {
|
|
446
|
+
return {
|
|
447
|
+
main: `<section class="view-surface flex min-h-full">${renderMissingDatabase()}</section>`,
|
|
448
|
+
panel: '',
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
main: `
|
|
454
|
+
<section class="charts-view">
|
|
455
|
+
<aside class="charts-view__sidebar">
|
|
456
|
+
<div class="charts-view__sidebar-header">
|
|
457
|
+
<div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
|
|
458
|
+
Query History
|
|
459
|
+
</div>
|
|
460
|
+
<h2 class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
461
|
+
Charts
|
|
462
|
+
</h2>
|
|
463
|
+
</div>
|
|
464
|
+
${renderChartsList(state)}
|
|
465
|
+
</aside>
|
|
466
|
+
<div class="charts-view__detail">${renderChartsDetail(state)}</div>
|
|
467
|
+
</section>
|
|
468
|
+
`,
|
|
469
|
+
panel: '',
|
|
470
|
+
};
|
|
471
|
+
}
|