sqlite-hub 0.6.0 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -0
- package/bin/sqlite-hub.js +285 -4
- package/changelog.md +21 -0
- package/docs/DESIGN_GUIDELINES.md +36 -0
- package/frontend/assets/mockups/sql_editor_croped.png +0 -0
- package/frontend/index.html +80 -0
- package/frontend/js/api.js +34 -0
- package/frontend/js/app.js +188 -10
- package/frontend/js/components/connectionCard.js +3 -4
- package/frontend/js/components/emptyState.js +4 -4
- package/frontend/js/components/modal.js +341 -24
- package/frontend/js/components/queryChartRenderer.js +125 -0
- package/frontend/js/components/queryEditor.js +33 -6
- package/frontend/js/components/queryHistoryDetail.js +16 -7
- package/frontend/js/components/queryHistoryPanel.js +18 -7
- package/frontend/js/components/rowEditorPanel.js +59 -54
- package/frontend/js/components/sidebar.js +32 -32
- package/frontend/js/components/structureGraph.js +54 -122
- package/frontend/js/components/tableDesignerEditor.js +9 -8
- package/frontend/js/components/tableDesignerSidebar.js +52 -48
- package/frontend/js/components/tableDesignerSqlPreview.js +60 -28
- package/frontend/js/lib/queryChartOptions.js +283 -0
- package/frontend/js/lib/queryCharts.js +560 -0
- package/frontend/js/router.js +8 -0
- package/frontend/js/store.js +641 -0
- package/frontend/js/views/charts.js +499 -0
- package/frontend/js/views/connections.js +5 -17
- package/frontend/js/views/data.js +40 -27
- package/frontend/js/views/editor.js +19 -13
- package/frontend/js/views/overview.js +149 -10
- package/frontend/js/views/settings.js +2 -2
- package/frontend/js/views/structure.js +105 -59
- package/frontend/js/views/tableDesigner.js +7 -2
- package/frontend/styles/base.css +62 -0
- package/frontend/styles/components.css +68 -118
- package/frontend/styles/structure-graph.css +19 -82
- package/frontend/styles/tokens.css +2 -0
- package/frontend/styles/views.css +293 -66
- package/package.json +2 -1
- package/server/routes/charts.js +153 -0
- package/server/server.js +6 -0
- package/server/services/sqlite/overviewService.js +68 -0
- package/server/services/storage/appStateStore.js +400 -1
- package/server/services/storage/queryHistoryChartUtils.js +145 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { escapeHtml, truncateMiddle } from "../utils/format.js";
|
|
2
2
|
import { renderConnectionLogo } from "./connectionLogo.js";
|
|
3
|
+
import {
|
|
4
|
+
analyzeQueryChartResult,
|
|
5
|
+
getQueryChartTypeLabel,
|
|
6
|
+
QUERY_CHART_TYPES,
|
|
7
|
+
} from "../lib/queryCharts.js";
|
|
3
8
|
|
|
4
9
|
function renderField({ label, name, type = "text", placeholder = "", value = "" }) {
|
|
5
10
|
return `
|
|
@@ -8,7 +13,7 @@ function renderField({ label, name, type = "text", placeholder = "", value = ""
|
|
|
8
13
|
${escapeHtml(label)}
|
|
9
14
|
</span>
|
|
10
15
|
<input
|
|
11
|
-
class="w-full border border-outline-variant/20 bg-surface-container-lowest
|
|
16
|
+
class="control-input w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
12
17
|
name="${escapeHtml(name)}"
|
|
13
18
|
placeholder="${escapeHtml(placeholder)}"
|
|
14
19
|
type="${escapeHtml(type)}"
|
|
@@ -20,14 +25,18 @@ function renderField({ label, name, type = "text", placeholder = "", value = ""
|
|
|
20
25
|
|
|
21
26
|
function renderCheckboxField({ label, name, checked = false, text }) {
|
|
22
27
|
return `
|
|
23
|
-
<label class="
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
<label class="block space-y-2">
|
|
29
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
30
|
+
${escapeHtml(label)}
|
|
31
|
+
</span>
|
|
32
|
+
<span class="standard-checkbox">
|
|
33
|
+
<input
|
|
34
|
+
${checked ? "checked" : ""}
|
|
35
|
+
name="${escapeHtml(name)}"
|
|
36
|
+
type="checkbox"
|
|
37
|
+
/>
|
|
38
|
+
<span>${escapeHtml(text || label)}</span>
|
|
39
|
+
</span>
|
|
31
40
|
</label>
|
|
32
41
|
`;
|
|
33
42
|
}
|
|
@@ -45,7 +54,7 @@ function renderFileField({
|
|
|
45
54
|
</span>
|
|
46
55
|
<input
|
|
47
56
|
accept="${escapeHtml(accept)}"
|
|
48
|
-
class="block w-full border border-outline-variant/20 bg-surface-container-lowest
|
|
57
|
+
class="control-input block w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface file:mr-4 file:border-0 file:bg-primary-container file:px-3 file:py-2 file:text-xs file:font-bold file:text-on-primary"
|
|
49
58
|
name="${escapeHtml(name)}"
|
|
50
59
|
type="file"
|
|
51
60
|
/>
|
|
@@ -58,6 +67,31 @@ function renderFileField({
|
|
|
58
67
|
`;
|
|
59
68
|
}
|
|
60
69
|
|
|
70
|
+
function renderSelectField({ label, name, value = "", options = [], bind = "" }) {
|
|
71
|
+
return `
|
|
72
|
+
<label class="block space-y-2">
|
|
73
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
74
|
+
${escapeHtml(label)}
|
|
75
|
+
</span>
|
|
76
|
+
<select
|
|
77
|
+
class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
78
|
+
${bind ? `data-bind="${escapeHtml(bind)}"` : ""}
|
|
79
|
+
${name ? `name="${escapeHtml(name)}"` : ""}
|
|
80
|
+
>
|
|
81
|
+
${options
|
|
82
|
+
.map(
|
|
83
|
+
(option) => `
|
|
84
|
+
<option value="${escapeHtml(option.value)}" ${String(option.value) === String(value) ? "selected" : ""}>
|
|
85
|
+
${escapeHtml(option.label)}
|
|
86
|
+
</option>
|
|
87
|
+
`
|
|
88
|
+
)
|
|
89
|
+
.join("")}
|
|
90
|
+
</select>
|
|
91
|
+
</label>
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
|
|
61
95
|
function renderError(error) {
|
|
62
96
|
if (!error) {
|
|
63
97
|
return "";
|
|
@@ -94,14 +128,14 @@ function renderOpenConnectionForm(modal) {
|
|
|
94
128
|
${renderError(modal.error)}
|
|
95
129
|
<div class="flex items-center justify-end gap-3 pt-2">
|
|
96
130
|
<button
|
|
97
|
-
class="
|
|
131
|
+
class="standard-button"
|
|
98
132
|
data-action="close-modal"
|
|
99
133
|
type="button"
|
|
100
134
|
>
|
|
101
135
|
Cancel
|
|
102
136
|
</button>
|
|
103
137
|
<button
|
|
104
|
-
class="
|
|
138
|
+
class="standard-button"
|
|
105
139
|
type="submit"
|
|
106
140
|
>
|
|
107
141
|
${modal.submitting ? "Opening..." : "Open Database"}
|
|
@@ -168,14 +202,14 @@ function renderEditConnectionForm(modal) {
|
|
|
168
202
|
${renderError(modal.error)}
|
|
169
203
|
<div class="flex items-center justify-end gap-3 pt-2">
|
|
170
204
|
<button
|
|
171
|
-
class="
|
|
205
|
+
class="standard-button"
|
|
172
206
|
data-action="close-modal"
|
|
173
207
|
type="button"
|
|
174
208
|
>
|
|
175
209
|
Cancel
|
|
176
210
|
</button>
|
|
177
211
|
<button
|
|
178
|
-
class="
|
|
212
|
+
class="standard-button"
|
|
179
213
|
type="submit"
|
|
180
214
|
>
|
|
181
215
|
${modal.submitting ? "Saving..." : "Save Changes"}
|
|
@@ -201,14 +235,14 @@ function renderCreateDatabaseForm(modal) {
|
|
|
201
235
|
${renderError(modal.error)}
|
|
202
236
|
<div class="flex items-center justify-end gap-3 pt-2">
|
|
203
237
|
<button
|
|
204
|
-
class="
|
|
238
|
+
class="standard-button"
|
|
205
239
|
data-action="close-modal"
|
|
206
240
|
type="button"
|
|
207
241
|
>
|
|
208
242
|
Cancel
|
|
209
243
|
</button>
|
|
210
244
|
<button
|
|
211
|
-
class="
|
|
245
|
+
class="standard-button"
|
|
212
246
|
type="submit"
|
|
213
247
|
>
|
|
214
248
|
${modal.submitting ? "Creating..." : "Create Database"}
|
|
@@ -235,7 +269,7 @@ function renderImportTargetOptions(state) {
|
|
|
235
269
|
Import Target
|
|
236
270
|
</span>
|
|
237
271
|
<select
|
|
238
|
-
class="w-full border border-outline-variant/20 bg-surface-container-lowest
|
|
272
|
+
class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
239
273
|
name="targetMode"
|
|
240
274
|
>
|
|
241
275
|
${
|
|
@@ -256,7 +290,7 @@ function renderImportTargetOptions(state) {
|
|
|
256
290
|
Recent Connection
|
|
257
291
|
</span>
|
|
258
292
|
<select
|
|
259
|
-
class="w-full border border-outline-variant/20 bg-surface-container-lowest
|
|
293
|
+
class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
260
294
|
name="targetConnectionId"
|
|
261
295
|
>
|
|
262
296
|
${recentOptions}
|
|
@@ -294,14 +328,14 @@ function renderImportSqlForm(modal, state) {
|
|
|
294
328
|
${renderError(modal.error)}
|
|
295
329
|
<div class="flex items-center justify-end gap-3 pt-2">
|
|
296
330
|
<button
|
|
297
|
-
class="
|
|
331
|
+
class="standard-button"
|
|
298
332
|
data-action="close-modal"
|
|
299
333
|
type="button"
|
|
300
334
|
>
|
|
301
335
|
Cancel
|
|
302
336
|
</button>
|
|
303
337
|
<button
|
|
304
|
-
class="
|
|
338
|
+
class="standard-button"
|
|
305
339
|
type="submit"
|
|
306
340
|
>
|
|
307
341
|
${modal.submitting ? "Importing..." : "Import SQL Dump"}
|
|
@@ -363,14 +397,14 @@ function renderDeleteRowConfirmForm(modal) {
|
|
|
363
397
|
${renderError(modal.error)}
|
|
364
398
|
<div class="flex items-center justify-end gap-3 pt-2">
|
|
365
399
|
<button
|
|
366
|
-
class="
|
|
400
|
+
class="standard-button"
|
|
367
401
|
data-action="close-modal"
|
|
368
402
|
type="button"
|
|
369
403
|
>
|
|
370
404
|
Cancel
|
|
371
405
|
</button>
|
|
372
406
|
<button
|
|
373
|
-
class="
|
|
407
|
+
class="delete-button"
|
|
374
408
|
type="submit"
|
|
375
409
|
>
|
|
376
410
|
${modal.submitting ? "Deleting..." : "Delete Row"}
|
|
@@ -380,6 +414,279 @@ function renderDeleteRowConfirmForm(modal) {
|
|
|
380
414
|
`;
|
|
381
415
|
}
|
|
382
416
|
|
|
417
|
+
function renderChartColumnOptions(analysis, { allowEmpty = false, includeNumericHint = false } = {}) {
|
|
418
|
+
const options = allowEmpty ? [{ value: "", label: "None" }] : [];
|
|
419
|
+
|
|
420
|
+
return options.concat(
|
|
421
|
+
(analysis?.columns ?? []).map((column) => ({
|
|
422
|
+
value: column.name,
|
|
423
|
+
label: `${column.name} (${column.type}${includeNumericHint && column.type === "number" ? " numeric" : ""})`,
|
|
424
|
+
}))
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function renderChartEditorForm(modal, state) {
|
|
429
|
+
const draft = modal.draft ?? {};
|
|
430
|
+
const analysis = state.charts.result ? analyzeQueryChartResult(state.charts.result) : null;
|
|
431
|
+
const chartTypeOptions = QUERY_CHART_TYPES.map((chartType) => ({
|
|
432
|
+
value: chartType,
|
|
433
|
+
label: getQueryChartTypeLabel(chartType),
|
|
434
|
+
}));
|
|
435
|
+
const columnOptions = renderChartColumnOptions(analysis);
|
|
436
|
+
const optionalColumnOptions = renderChartColumnOptions(analysis, { allowEmpty: true });
|
|
437
|
+
let chartSpecificFields = "";
|
|
438
|
+
|
|
439
|
+
if (draft.chartType === "bar") {
|
|
440
|
+
chartSpecificFields = `
|
|
441
|
+
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
442
|
+
${renderSelectField({
|
|
443
|
+
label: "X Column",
|
|
444
|
+
value: draft.config?.x_column ?? "",
|
|
445
|
+
options: columnOptions,
|
|
446
|
+
bind: "query-chart-draft-config:x_column",
|
|
447
|
+
})}
|
|
448
|
+
${renderSelectField({
|
|
449
|
+
label: "Y Column",
|
|
450
|
+
value: draft.config?.y_column ?? "",
|
|
451
|
+
options: columnOptions,
|
|
452
|
+
bind: "query-chart-draft-config:y_column",
|
|
453
|
+
})}
|
|
454
|
+
</div>
|
|
455
|
+
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
456
|
+
${renderSelectField({
|
|
457
|
+
label: "Sort Direction",
|
|
458
|
+
value: draft.config?.sort_direction ?? "asc",
|
|
459
|
+
options: [
|
|
460
|
+
{ value: "asc", label: "Ascending" },
|
|
461
|
+
{ value: "desc", label: "Descending" },
|
|
462
|
+
],
|
|
463
|
+
bind: "query-chart-draft-config:sort_direction",
|
|
464
|
+
})}
|
|
465
|
+
${renderCheckboxField({
|
|
466
|
+
label: "Show legend",
|
|
467
|
+
name: "",
|
|
468
|
+
checked: Boolean(draft.config?.show_legend),
|
|
469
|
+
text: "Show legend",
|
|
470
|
+
}).replace("<input", '<input data-bind="query-chart-draft-config:show_legend"')}
|
|
471
|
+
${renderCheckboxField({
|
|
472
|
+
label: "Show labels",
|
|
473
|
+
name: "",
|
|
474
|
+
checked: Boolean(draft.config?.show_labels),
|
|
475
|
+
text: "Show labels",
|
|
476
|
+
}).replace("<input", '<input data-bind="query-chart-draft-config:show_labels"')}
|
|
477
|
+
</div>
|
|
478
|
+
`;
|
|
479
|
+
} else if (draft.chartType === "line") {
|
|
480
|
+
chartSpecificFields = `
|
|
481
|
+
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
482
|
+
${renderSelectField({
|
|
483
|
+
label: "X Column",
|
|
484
|
+
value: draft.config?.x_column ?? "",
|
|
485
|
+
options: columnOptions,
|
|
486
|
+
bind: "query-chart-draft-config:x_column",
|
|
487
|
+
})}
|
|
488
|
+
${renderSelectField({
|
|
489
|
+
label: "Y Column",
|
|
490
|
+
value: draft.config?.y_column ?? "",
|
|
491
|
+
options: columnOptions,
|
|
492
|
+
bind: "query-chart-draft-config:y_column",
|
|
493
|
+
})}
|
|
494
|
+
</div>
|
|
495
|
+
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
|
|
496
|
+
${renderSelectField({
|
|
497
|
+
label: "Sort Direction",
|
|
498
|
+
value: draft.config?.sort_direction ?? "asc",
|
|
499
|
+
options: [
|
|
500
|
+
{ value: "asc", label: "Ascending" },
|
|
501
|
+
{ value: "desc", label: "Descending" },
|
|
502
|
+
],
|
|
503
|
+
bind: "query-chart-draft-config:sort_direction",
|
|
504
|
+
})}
|
|
505
|
+
${renderCheckboxField({
|
|
506
|
+
label: "Smooth line",
|
|
507
|
+
name: "",
|
|
508
|
+
checked: Boolean(draft.config?.smooth),
|
|
509
|
+
text: "Smooth line",
|
|
510
|
+
}).replace("<input", '<input data-bind="query-chart-draft-config:smooth"')}
|
|
511
|
+
${renderCheckboxField({
|
|
512
|
+
label: "Show legend",
|
|
513
|
+
name: "",
|
|
514
|
+
checked: Boolean(draft.config?.show_legend),
|
|
515
|
+
text: "Show legend",
|
|
516
|
+
}).replace("<input", '<input data-bind="query-chart-draft-config:show_legend"')}
|
|
517
|
+
${renderCheckboxField({
|
|
518
|
+
label: "Show labels",
|
|
519
|
+
name: "",
|
|
520
|
+
checked: Boolean(draft.config?.show_labels),
|
|
521
|
+
text: "Show labels",
|
|
522
|
+
}).replace("<input", '<input data-bind="query-chart-draft-config:show_labels"')}
|
|
523
|
+
</div>
|
|
524
|
+
`;
|
|
525
|
+
} else if (draft.chartType === "pie") {
|
|
526
|
+
chartSpecificFields = `
|
|
527
|
+
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
528
|
+
${renderSelectField({
|
|
529
|
+
label: "Label Column",
|
|
530
|
+
value: draft.config?.label_column ?? "",
|
|
531
|
+
options: columnOptions,
|
|
532
|
+
bind: "query-chart-draft-config:label_column",
|
|
533
|
+
})}
|
|
534
|
+
${renderSelectField({
|
|
535
|
+
label: "Value Column",
|
|
536
|
+
value: draft.config?.value_column ?? "",
|
|
537
|
+
options: columnOptions,
|
|
538
|
+
bind: "query-chart-draft-config:value_column",
|
|
539
|
+
})}
|
|
540
|
+
</div>
|
|
541
|
+
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
542
|
+
${renderCheckboxField({
|
|
543
|
+
label: "Donut",
|
|
544
|
+
name: "",
|
|
545
|
+
checked: Boolean(draft.config?.donut),
|
|
546
|
+
text: "Render as donut",
|
|
547
|
+
}).replace("<input", '<input data-bind="query-chart-draft-config:donut"')}
|
|
548
|
+
${renderCheckboxField({
|
|
549
|
+
label: "Show legend",
|
|
550
|
+
name: "",
|
|
551
|
+
checked: Boolean(draft.config?.show_legend),
|
|
552
|
+
text: "Show legend",
|
|
553
|
+
}).replace("<input", '<input data-bind="query-chart-draft-config:show_legend"')}
|
|
554
|
+
${renderCheckboxField({
|
|
555
|
+
label: "Show labels",
|
|
556
|
+
name: "",
|
|
557
|
+
checked: Boolean(draft.config?.show_labels),
|
|
558
|
+
text: "Show labels",
|
|
559
|
+
}).replace("<input", '<input data-bind="query-chart-draft-config:show_labels"')}
|
|
560
|
+
</div>
|
|
561
|
+
`;
|
|
562
|
+
} else if (draft.chartType === "scatter") {
|
|
563
|
+
chartSpecificFields = `
|
|
564
|
+
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
565
|
+
${renderSelectField({
|
|
566
|
+
label: "X Column",
|
|
567
|
+
value: draft.config?.x_column ?? "",
|
|
568
|
+
options: columnOptions,
|
|
569
|
+
bind: "query-chart-draft-config:x_column",
|
|
570
|
+
})}
|
|
571
|
+
${renderSelectField({
|
|
572
|
+
label: "Y Column",
|
|
573
|
+
value: draft.config?.y_column ?? "",
|
|
574
|
+
options: columnOptions,
|
|
575
|
+
bind: "query-chart-draft-config:y_column",
|
|
576
|
+
})}
|
|
577
|
+
</div>
|
|
578
|
+
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
579
|
+
${renderSelectField({
|
|
580
|
+
label: "Size Column",
|
|
581
|
+
value: draft.config?.size_column ?? "",
|
|
582
|
+
options: optionalColumnOptions,
|
|
583
|
+
bind: "query-chart-draft-config:size_column",
|
|
584
|
+
})}
|
|
585
|
+
${renderSelectField({
|
|
586
|
+
label: "Series Column",
|
|
587
|
+
value: draft.config?.series_column ?? "",
|
|
588
|
+
options: optionalColumnOptions,
|
|
589
|
+
bind: "query-chart-draft-config:series_column",
|
|
590
|
+
})}
|
|
591
|
+
${renderCheckboxField({
|
|
592
|
+
label: "Show legend",
|
|
593
|
+
name: "",
|
|
594
|
+
checked: Boolean(draft.config?.show_legend),
|
|
595
|
+
text: "Show legend",
|
|
596
|
+
}).replace("<input", '<input data-bind="query-chart-draft-config:show_legend"')}
|
|
597
|
+
</div>
|
|
598
|
+
`;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return `
|
|
602
|
+
<form class="space-y-5" data-form="save-query-chart">
|
|
603
|
+
${renderField({
|
|
604
|
+
label: "Chart Name",
|
|
605
|
+
name: "chartName",
|
|
606
|
+
value: draft.name ?? "",
|
|
607
|
+
}).replace("<input", '<input data-bind="query-chart-draft:name"')}
|
|
608
|
+
${renderSelectField({
|
|
609
|
+
label: "Chart Type",
|
|
610
|
+
value: draft.chartType ?? "bar",
|
|
611
|
+
options: chartTypeOptions,
|
|
612
|
+
bind: "query-chart-draft:chartType",
|
|
613
|
+
})}
|
|
614
|
+
${chartSpecificFields}
|
|
615
|
+
${
|
|
616
|
+
analysis
|
|
617
|
+
? `
|
|
618
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-4">
|
|
619
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
620
|
+
Result Columns
|
|
621
|
+
</div>
|
|
622
|
+
<div class="mt-3 flex flex-wrap gap-2">
|
|
623
|
+
${analysis.columns
|
|
624
|
+
.map(
|
|
625
|
+
(column) => `
|
|
626
|
+
<span class="border border-outline-variant/15 bg-surface-container px-2 py-1 text-[10px] font-mono uppercase tracking-[0.12em] text-on-surface-variant/70">
|
|
627
|
+
${escapeHtml(column.name)} • ${escapeHtml(column.type)}
|
|
628
|
+
</span>
|
|
629
|
+
`
|
|
630
|
+
)
|
|
631
|
+
.join("")}
|
|
632
|
+
</div>
|
|
633
|
+
</div>
|
|
634
|
+
`
|
|
635
|
+
: ""
|
|
636
|
+
}
|
|
637
|
+
${renderError(modal.error)}
|
|
638
|
+
<div class="flex items-center justify-end gap-3 pt-2">
|
|
639
|
+
<button
|
|
640
|
+
class="standard-button"
|
|
641
|
+
data-action="close-modal"
|
|
642
|
+
type="button"
|
|
643
|
+
>
|
|
644
|
+
Cancel
|
|
645
|
+
</button>
|
|
646
|
+
<button
|
|
647
|
+
class="standard-button"
|
|
648
|
+
type="submit"
|
|
649
|
+
>
|
|
650
|
+
${modal.submitting ? "Saving..." : draft.mode === "edit" ? "Save Chart" : "Create Chart"}
|
|
651
|
+
</button>
|
|
652
|
+
</div>
|
|
653
|
+
</form>
|
|
654
|
+
`;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function renderDeleteChartForm(modal) {
|
|
658
|
+
return `
|
|
659
|
+
<form class="space-y-5" data-form="delete-query-chart">
|
|
660
|
+
<div class="space-y-3">
|
|
661
|
+
<p class="text-sm leading-7 text-on-surface">
|
|
662
|
+
Delete chart <span class="font-bold text-primary-container">${escapeHtml(
|
|
663
|
+
modal.chartName ?? "Chart"
|
|
664
|
+
)}</span>?
|
|
665
|
+
</p>
|
|
666
|
+
<p class="text-sm leading-7 text-on-surface-variant/65">
|
|
667
|
+
The linked query-history entry stays intact. Only this chart definition is removed.
|
|
668
|
+
</p>
|
|
669
|
+
</div>
|
|
670
|
+
${renderError(modal.error)}
|
|
671
|
+
<div class="flex items-center justify-end gap-3 pt-2">
|
|
672
|
+
<button
|
|
673
|
+
class="standard-button"
|
|
674
|
+
data-action="close-modal"
|
|
675
|
+
type="button"
|
|
676
|
+
>
|
|
677
|
+
Cancel
|
|
678
|
+
</button>
|
|
679
|
+
<button
|
|
680
|
+
class="delete-button"
|
|
681
|
+
type="submit"
|
|
682
|
+
>
|
|
683
|
+
${modal.submitting ? "Deleting..." : "Delete Chart"}
|
|
684
|
+
</button>
|
|
685
|
+
</div>
|
|
686
|
+
</form>
|
|
687
|
+
`;
|
|
688
|
+
}
|
|
689
|
+
|
|
383
690
|
export function renderModal(state) {
|
|
384
691
|
const modal = state.modal;
|
|
385
692
|
|
|
@@ -413,6 +720,16 @@ export function renderModal(state) {
|
|
|
413
720
|
title: "Delete Row",
|
|
414
721
|
body: renderDeleteRowConfirmForm(modal),
|
|
415
722
|
},
|
|
723
|
+
"chart-editor": {
|
|
724
|
+
eyebrow: "Charts // Configure query-based ECharts panel",
|
|
725
|
+
title: modal.draft?.mode === "edit" ? "Edit Chart" : "New Chart",
|
|
726
|
+
body: renderChartEditorForm(modal, state),
|
|
727
|
+
},
|
|
728
|
+
"delete-chart": {
|
|
729
|
+
eyebrow: "Charts // Confirm chart deletion",
|
|
730
|
+
title: "Delete Chart",
|
|
731
|
+
body: renderDeleteChartForm(modal),
|
|
732
|
+
},
|
|
416
733
|
};
|
|
417
734
|
|
|
418
735
|
const config = contentByKind[modal.kind];
|
|
@@ -423,7 +740,7 @@ export function renderModal(state) {
|
|
|
423
740
|
|
|
424
741
|
return `
|
|
425
742
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-background/85 px-4 backdrop-blur-sm">
|
|
426
|
-
<div class="w-full max-w-xl border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
|
|
743
|
+
<div class="w-full ${modal.kind === "chart-editor" ? "max-w-3xl" : "max-w-xl"} border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
|
|
427
744
|
<div class="flex items-start justify-between gap-4 border-b border-outline-variant/10 bg-surface-container-low px-6 py-5">
|
|
428
745
|
<div>
|
|
429
746
|
<div class="text-[10px] font-mono uppercase tracking-[0.26em] text-primary-container/70">
|
|
@@ -434,7 +751,7 @@ export function renderModal(state) {
|
|
|
434
751
|
</h2>
|
|
435
752
|
</div>
|
|
436
753
|
<button
|
|
437
|
-
class="
|
|
754
|
+
class="control-icon-button border border-outline-variant/20 text-on-surface-variant hover:bg-surface-container-highest hover:text-primary-container"
|
|
438
755
|
data-action="close-modal"
|
|
439
756
|
type="button"
|
|
440
757
|
>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildBarChartOption,
|
|
3
|
+
buildLineChartOption,
|
|
4
|
+
buildPieChartOption,
|
|
5
|
+
buildScatterChartOption,
|
|
6
|
+
} from "../lib/queryChartOptions.js";
|
|
7
|
+
import { analyzeQueryChartResult, validateQueryChartConfig } from "../lib/queryCharts.js";
|
|
8
|
+
|
|
9
|
+
const chartInstances = new Map();
|
|
10
|
+
const resizeObservers = new Map();
|
|
11
|
+
|
|
12
|
+
function getEchartsRuntime() {
|
|
13
|
+
return window.echarts ?? null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getOptionBuilder(chartType) {
|
|
17
|
+
if (chartType === "bar") {
|
|
18
|
+
return buildBarChartOption;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (chartType === "line") {
|
|
22
|
+
return buildLineChartOption;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (chartType === "pie") {
|
|
26
|
+
return buildPieChartOption;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (chartType === "scatter") {
|
|
30
|
+
return buildScatterChartOption;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function disposeChart(chartId) {
|
|
37
|
+
const instance = chartInstances.get(chartId);
|
|
38
|
+
|
|
39
|
+
if (instance) {
|
|
40
|
+
instance.dispose();
|
|
41
|
+
chartInstances.delete(chartId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const observer = resizeObservers.get(chartId);
|
|
45
|
+
|
|
46
|
+
if (observer) {
|
|
47
|
+
observer.disconnect();
|
|
48
|
+
resizeObservers.delete(chartId);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function teardownQueryChartRenderer() {
|
|
53
|
+
Array.from(chartInstances.keys()).forEach((chartId) => disposeChart(chartId));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function mountQueryChartRenderer(state) {
|
|
57
|
+
const echartsRuntime = getEchartsRuntime();
|
|
58
|
+
const charts = state.charts.detail?.charts ?? [];
|
|
59
|
+
const result = state.charts.result;
|
|
60
|
+
|
|
61
|
+
teardownQueryChartRenderer();
|
|
62
|
+
|
|
63
|
+
if (!echartsRuntime || !result || !charts.length) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const analysis = analyzeQueryChartResult(result);
|
|
68
|
+
|
|
69
|
+
charts.forEach((chart) => {
|
|
70
|
+
const host = document.querySelector(`[data-query-chart-id="${CSS.escape(String(chart.id))}"]`);
|
|
71
|
+
|
|
72
|
+
if (!(host instanceof HTMLElement)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const validation = validateQueryChartConfig(chart.chartType, chart.config, analysis);
|
|
77
|
+
|
|
78
|
+
if (!validation.valid) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const buildOption = getOptionBuilder(chart.chartType);
|
|
83
|
+
|
|
84
|
+
if (!buildOption) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const instance = echartsRuntime.init(host);
|
|
89
|
+
|
|
90
|
+
instance.setOption(buildOption(chart, result.rows ?? [], analysis), true);
|
|
91
|
+
chartInstances.set(String(chart.id), instance);
|
|
92
|
+
|
|
93
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
94
|
+
instance.resize();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
resizeObserver.observe(host);
|
|
98
|
+
resizeObservers.set(String(chart.id), resizeObserver);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function exportQueryChartAsPng(chartId) {
|
|
103
|
+
const instance = chartInstances.get(String(chartId));
|
|
104
|
+
|
|
105
|
+
if (!instance) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const link = document.createElement("a");
|
|
110
|
+
const chartNode = document.querySelector(
|
|
111
|
+
`[data-query-chart-id="${CSS.escape(String(chartId))}"]`
|
|
112
|
+
);
|
|
113
|
+
const fileName = chartNode?.dataset.chartExportName ?? `chart-${chartId}`;
|
|
114
|
+
|
|
115
|
+
link.href = instance.getDataURL({
|
|
116
|
+
type: "png",
|
|
117
|
+
pixelRatio: 2,
|
|
118
|
+
backgroundColor: "#131313",
|
|
119
|
+
});
|
|
120
|
+
link.download = `${fileName}.png`;
|
|
121
|
+
document.body.appendChild(link);
|
|
122
|
+
link.click();
|
|
123
|
+
link.remove();
|
|
124
|
+
return true;
|
|
125
|
+
}
|