sqlite-hub 0.9.1 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +36 -0
- package/README.md +2 -2
- package/bin/sqlite-hub.js +1 -1
- package/frontend/index.html +3 -158
- package/frontend/js/app.js +231 -5
- package/frontend/js/components/connectionCard.js +62 -87
- package/frontend/js/components/emptyState.js +20 -23
- package/frontend/js/components/modal.js +158 -199
- package/frontend/js/components/pageHeader.js +1 -1
- package/frontend/js/components/queryEditor.js +16 -30
- package/frontend/js/components/queryHistoryDetail.js +93 -164
- package/frontend/js/components/queryHistoryPanel.js +90 -100
- package/frontend/js/components/queryResults.js +3 -1
- package/frontend/js/components/rowEditorPanel.js +76 -56
- package/frontend/js/components/tableDesignerEditor.js +91 -116
- package/frontend/js/lib/queryChartOptions.js +5 -1
- package/frontend/js/lib/queryCharts.js +50 -2
- package/frontend/js/store.js +161 -23
- package/frontend/js/utils/tableDesigner.js +8 -2
- package/frontend/js/views/charts.js +126 -73
- package/frontend/js/views/data.js +147 -133
- package/frontend/js/views/editor.js +1 -0
- package/frontend/js/views/mediaTagging.js +131 -164
- package/frontend/js/views/structure.js +52 -48
- package/frontend/styles/base.css +57 -13
- package/frontend/styles/components.css +166 -96
- package/frontend/styles/layout.css +1 -1
- package/frontend/styles/structure-graph.css +11 -11
- package/frontend/styles/tailwind.css +81 -0
- package/frontend/styles/tailwind.generated.css +1 -0
- package/frontend/styles/tokens.css +50 -7
- package/frontend/styles/utilities.css +21 -0
- package/frontend/styles/views.css +94 -86
- package/package.json +16 -3
- package/server/routes/mediaTagging.js +2 -10
- package/server/routes/sql.js +35 -10
- package/server/server.js +24 -0
- package/server/services/sqlite/dataBrowserService.js +25 -5
- package/server/services/sqlite/exportService.js +4 -2
- package/server/services/sqlite/introspection.js +2 -2
- package/server/services/sqlite/mediaTaggingService.js +166 -53
- package/server/services/sqlite/structureService.js +2 -2
- package/server/services/sqlite/tableDesigner/sql.js +19 -3
- package/server/services/storage/appStateStore.js +227 -87
- package/server/services/storage/queryHistoryChartUtils.js +19 -2
- package/server/utils/appPaths.js +55 -19
- package/server/utils/fileValidation.js +94 -8
- package/tailwind.config.cjs +73 -0
- package/tests/security-paths.test.js +84 -0
- package/tests/sql-identifier-safety.test.js +66 -0
- package/.npmingnore +0 -4
- package/changelog.md +0 -77
- package/docs/DESIGN_GUIDELINES.md +0 -36
- package/scripts/publish_brew.sh +0 -466
- package/scripts/publish_npm.sh +0 -241
- package/shortkeys.md +0 -5
|
@@ -13,92 +13,67 @@ export function renderConnectionCard(connection, activeConnectionId) {
|
|
|
13
13
|
: Boolean(connection.isActive);
|
|
14
14
|
const clipPath = "polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%)";
|
|
15
15
|
const primaryActionLabel = isActive ? "Open Overview" : "Set Active";
|
|
16
|
+
const connectionId = escapeHtml(connection.id);
|
|
17
|
+
const logoMarkup = renderConnectionLogo(connection, {
|
|
18
|
+
containerClass: [
|
|
19
|
+
"clipped-corner flex h-10 w-10 items-center justify-center overflow-hidden transition-colors",
|
|
20
|
+
isActive ? "bg-primary-container" : "bg-surface-container-highest",
|
|
21
|
+
].join(" "),
|
|
22
|
+
containerAttributes: ['style="--clip-path: ', clipPath, ';"'].join(""),
|
|
23
|
+
imageClassName: "h-full w-full object-cover",
|
|
24
|
+
iconClassName: isActive ? "text-on-primary" : "text-outline-variant",
|
|
25
|
+
});
|
|
16
26
|
|
|
17
|
-
return
|
|
18
|
-
<article
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<div class="text-xs font-bold text-on-surface">${connection.readOnly ? "Read only" : "Read / Write"}</div>
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
72
|
-
</div>
|
|
73
|
-
<div class="border-t border-outline-variant/10 bg-surface-container-low px-4 py-3">
|
|
74
|
-
<div class="grid grid-cols-[minmax(0,1fr)_5.1rem_5.8rem] gap-2">
|
|
75
|
-
<button
|
|
76
|
-
class="signature-button"
|
|
77
|
-
data-action="select-connection"
|
|
78
|
-
data-connection-id="${escapeHtml(connection.id)}"
|
|
79
|
-
type="button"
|
|
80
|
-
title="${primaryActionLabel}"
|
|
81
|
-
>
|
|
82
|
-
${primaryActionLabel}
|
|
83
|
-
</button>
|
|
84
|
-
<button
|
|
85
|
-
class="standard-button"
|
|
86
|
-
data-action="edit-connection"
|
|
87
|
-
data-connection-id="${escapeHtml(connection.id)}"
|
|
88
|
-
type="button"
|
|
89
|
-
>
|
|
90
|
-
Edit
|
|
91
|
-
</button>
|
|
92
|
-
<button
|
|
93
|
-
class="delete-button"
|
|
94
|
-
data-action="remove-connection"
|
|
95
|
-
data-connection-id="${escapeHtml(connection.id)}"
|
|
96
|
-
type="button"
|
|
97
|
-
>
|
|
98
|
-
Remove
|
|
99
|
-
</button>
|
|
100
|
-
</div>
|
|
101
|
-
</div>
|
|
102
|
-
</article>
|
|
103
|
-
`;
|
|
27
|
+
return [
|
|
28
|
+
'<article class="connection-card clipped-corner ',
|
|
29
|
+
isActive ? "is-active" : "",
|
|
30
|
+
'" style="--clip-path: ',
|
|
31
|
+
clipPath,
|
|
32
|
+
';">',
|
|
33
|
+
'<div class="flex-1 p-6"><div class="mb-6 flex items-start justify-between">',
|
|
34
|
+
logoMarkup,
|
|
35
|
+
'<div class="flex items-center gap-2">',
|
|
36
|
+
renderStatusBadge(isActive ? "ACTIVE" : "RECENT", isActive ? "primary" : "muted"),
|
|
37
|
+
connection.readOnly ? renderStatusBadge("READ_ONLY", "alert") : "",
|
|
38
|
+
"</div></div>",
|
|
39
|
+
'<h3 class="mb-1 font-headline text-xl font-bold uppercase ',
|
|
40
|
+
isActive ? "text-[#FCE300]" : "text-on-surface",
|
|
41
|
+
'">',
|
|
42
|
+
escapeHtml(connection.label),
|
|
43
|
+
"</h3>",
|
|
44
|
+
'<p class="block overflow-hidden text-ellipsis whitespace-nowrap font-mono text-[10px] text-outline-variant" title="',
|
|
45
|
+
escapeHtml(connection.path),
|
|
46
|
+
'">',
|
|
47
|
+
escapeHtml(truncateMiddle(connection.path, 68)),
|
|
48
|
+
"</p>",
|
|
49
|
+
'<div class="mt-8 grid grid-cols-2 gap-4">',
|
|
50
|
+
'<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Allocation</div><div class="text-xs font-bold text-on-surface">',
|
|
51
|
+
escapeHtml(formatBytes(connection.sizeBytes)),
|
|
52
|
+
"</div></div>",
|
|
53
|
+
'<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Last Modified</div><div class="text-xs font-bold text-on-surface">',
|
|
54
|
+
escapeHtml(formatDateTime(connection.lastModifiedAt)),
|
|
55
|
+
"</div></div>",
|
|
56
|
+
'<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Last Opened</div><div class="text-xs font-bold text-on-surface">',
|
|
57
|
+
escapeHtml(formatDateTime(connection.lastOpenedAt)),
|
|
58
|
+
"</div></div>",
|
|
59
|
+
'<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Mode</div><div class="text-xs font-bold text-on-surface">',
|
|
60
|
+
connection.readOnly ? "Read only" : "Read / Write",
|
|
61
|
+
"</div></div></div></div>",
|
|
62
|
+
'<div class="border-t border-outline-variant/10 bg-surface-container-low px-4 py-3">',
|
|
63
|
+
'<div class="grid grid-cols-[minmax(0,1fr)_5.1rem_5.8rem] gap-2">',
|
|
64
|
+
'<button class="signature-button" data-action="select-connection" data-connection-id="',
|
|
65
|
+
connectionId,
|
|
66
|
+
'" type="button" title="',
|
|
67
|
+
primaryActionLabel,
|
|
68
|
+
'">',
|
|
69
|
+
primaryActionLabel,
|
|
70
|
+
"</button>",
|
|
71
|
+
'<button class="standard-button" data-action="edit-connection" data-connection-id="',
|
|
72
|
+
connectionId,
|
|
73
|
+
'" type="button">Edit</button>',
|
|
74
|
+
'<button class="delete-button" data-action="remove-connection" data-connection-id="',
|
|
75
|
+
connectionId,
|
|
76
|
+
'" type="button">Remove</button>',
|
|
77
|
+
"</div></div></article>",
|
|
78
|
+
].join("");
|
|
104
79
|
}
|
|
@@ -6,6 +6,25 @@ import {
|
|
|
6
6
|
} from "../utils/format.js";
|
|
7
7
|
import { renderConnectionLogo } from "./connectionLogo.js";
|
|
8
8
|
|
|
9
|
+
function renderRecentConnectionButton(connection) {
|
|
10
|
+
return [
|
|
11
|
+
'<button class="control-button flex items-center gap-2 border border-outline-variant/15 bg-surface-container-low px-4 text-left text-on-surface transition-colors hover:border-primary-container/30 hover:bg-surface-container-high" data-action="select-connection" data-connection-id="',
|
|
12
|
+
escapeHtml(connection.id),
|
|
13
|
+
'" type="button">',
|
|
14
|
+
renderConnectionLogo(connection, {
|
|
15
|
+
containerClass:
|
|
16
|
+
"flex h-8 w-8 items-center justify-center overflow-hidden bg-surface-container-highest",
|
|
17
|
+
imageClassName: "h-full w-full object-cover",
|
|
18
|
+
iconClassName: "text-sm text-primary-container",
|
|
19
|
+
}),
|
|
20
|
+
'<span class="min-w-0"><span class="block truncate font-mono text-xs">',
|
|
21
|
+
escapeHtml(connection.label),
|
|
22
|
+
'</span><span class="block truncate text-[10px] text-on-surface-variant/45">',
|
|
23
|
+
escapeHtml(truncateMiddle(connection.path, 34)),
|
|
24
|
+
"</span></span></button>",
|
|
25
|
+
].join("");
|
|
26
|
+
}
|
|
27
|
+
|
|
9
28
|
function renderRecentConnections(recentConnections = []) {
|
|
10
29
|
if (!recentConnections.length) {
|
|
11
30
|
return `
|
|
@@ -19,29 +38,7 @@ function renderRecentConnections(recentConnections = []) {
|
|
|
19
38
|
<div class="flex flex-wrap justify-center gap-4">
|
|
20
39
|
${recentConnections
|
|
21
40
|
.slice(0, 4)
|
|
22
|
-
.map(
|
|
23
|
-
(connection) => `
|
|
24
|
-
<button
|
|
25
|
-
class="control-button flex items-center gap-2 border border-outline-variant/15 bg-surface-container-low px-4 text-left text-on-surface transition-colors hover:border-primary-container/30 hover:bg-surface-container-high"
|
|
26
|
-
data-action="select-connection"
|
|
27
|
-
data-connection-id="${escapeHtml(connection.id)}"
|
|
28
|
-
type="button"
|
|
29
|
-
>
|
|
30
|
-
${renderConnectionLogo(connection, {
|
|
31
|
-
containerClass:
|
|
32
|
-
"flex h-8 w-8 items-center justify-center overflow-hidden bg-surface-container-highest",
|
|
33
|
-
imageClassName: "h-full w-full object-cover",
|
|
34
|
-
iconClassName: "text-sm text-primary-container",
|
|
35
|
-
})}
|
|
36
|
-
<span class="min-w-0">
|
|
37
|
-
<span class="block truncate font-mono text-xs">${escapeHtml(connection.label)}</span>
|
|
38
|
-
<span class="block truncate text-[10px] text-on-surface-variant/45">${escapeHtml(
|
|
39
|
-
truncateMiddle(connection.path, 34)
|
|
40
|
-
)}</span>
|
|
41
|
-
</span>
|
|
42
|
-
</button>
|
|
43
|
-
`
|
|
44
|
-
)
|
|
41
|
+
.map((connection) => renderRecentConnectionButton(connection))
|
|
45
42
|
.join("")}
|
|
46
43
|
</div>
|
|
47
44
|
`;
|
|
@@ -97,28 +97,37 @@ function renderFileField({
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
function renderSelectField({ label, name, value = "", options = [], bind = "" }) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
100
|
+
const attributes = [
|
|
101
|
+
bind ? ['data-bind="', escapeHtml(bind), '"'].join("") : "",
|
|
102
|
+
name ? ['name="', escapeHtml(name), '"'].join("") : "",
|
|
103
|
+
]
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
.join(" ");
|
|
106
|
+
const optionMarkup = options
|
|
107
|
+
.map((option) =>
|
|
108
|
+
[
|
|
109
|
+
'<option value="',
|
|
110
|
+
escapeHtml(option.value),
|
|
111
|
+
'" ',
|
|
112
|
+
String(option.value) === String(value) ? "selected" : "",
|
|
113
|
+
">",
|
|
114
|
+
escapeHtml(option.label),
|
|
115
|
+
"</option>",
|
|
116
|
+
].join("")
|
|
117
|
+
)
|
|
118
|
+
.join("");
|
|
119
|
+
|
|
120
|
+
return [
|
|
121
|
+
'<label class="block space-y-2">',
|
|
122
|
+
'<span class="block text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">',
|
|
123
|
+
escapeHtml(label),
|
|
124
|
+
"</span>",
|
|
125
|
+
'<select 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" ',
|
|
126
|
+
attributes,
|
|
127
|
+
">",
|
|
128
|
+
optionMarkup,
|
|
129
|
+
"</select></label>",
|
|
130
|
+
].join("");
|
|
122
131
|
}
|
|
123
132
|
|
|
124
133
|
function renderError(error) {
|
|
@@ -283,62 +292,55 @@ function renderCreateDatabaseForm(modal) {
|
|
|
283
292
|
|
|
284
293
|
function renderImportTargetOptions(state) {
|
|
285
294
|
const recentOptions = state.connections.recent
|
|
286
|
-
.map(
|
|
287
|
-
|
|
288
|
-
<option value="
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
295
|
+
.map((connection) =>
|
|
296
|
+
[
|
|
297
|
+
'<option value="',
|
|
298
|
+
escapeHtml(connection.id),
|
|
299
|
+
'">',
|
|
300
|
+
escapeHtml(connection.label),
|
|
301
|
+
" • ",
|
|
302
|
+
escapeHtml(truncateMiddle(connection.path, 42)),
|
|
303
|
+
"</option>",
|
|
304
|
+
].join("")
|
|
292
305
|
)
|
|
293
306
|
.join("");
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
<select
|
|
322
|
-
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"
|
|
323
|
-
name="targetConnectionId"
|
|
324
|
-
>
|
|
325
|
-
${recentOptions}
|
|
326
|
-
</select>
|
|
327
|
-
</label>
|
|
328
|
-
`
|
|
329
|
-
: ""
|
|
330
|
-
}
|
|
331
|
-
${renderField({
|
|
307
|
+
const activeOption = state.connections.active
|
|
308
|
+
? '<option value="active">Use active database</option>'
|
|
309
|
+
: "";
|
|
310
|
+
const recentModeOption = state.connections.recent.length
|
|
311
|
+
? '<option value="recent">Use recent connection</option>'
|
|
312
|
+
: "";
|
|
313
|
+
const recentConnectionSelect = state.connections.recent.length
|
|
314
|
+
? [
|
|
315
|
+
'<label class="block space-y-2">',
|
|
316
|
+
'<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">Recent Connection</span>',
|
|
317
|
+
'<select 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" name="targetConnectionId">',
|
|
318
|
+
recentOptions,
|
|
319
|
+
"</select></label>",
|
|
320
|
+
].join("")
|
|
321
|
+
: "";
|
|
322
|
+
|
|
323
|
+
return [
|
|
324
|
+
'<label class="block space-y-2">',
|
|
325
|
+
'<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">Import Target</span>',
|
|
326
|
+
'<select 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" name="targetMode">',
|
|
327
|
+
activeOption,
|
|
328
|
+
recentModeOption,
|
|
329
|
+
'<option value="create">Create new database from dump</option>',
|
|
330
|
+
'<option value="path">Open explicit target path</option>',
|
|
331
|
+
"</select></label>",
|
|
332
|
+
recentConnectionSelect,
|
|
333
|
+
renderField({
|
|
332
334
|
label: "Target Path",
|
|
333
335
|
name: "targetPath",
|
|
334
336
|
placeholder: "/absolute/path/to/target.sqlite",
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
+
}),
|
|
338
|
+
renderField({
|
|
337
339
|
label: "Target Label",
|
|
338
340
|
name: "label",
|
|
339
341
|
placeholder: "Optional display name",
|
|
340
|
-
})
|
|
341
|
-
|
|
342
|
+
}),
|
|
343
|
+
].join("");
|
|
342
344
|
}
|
|
343
345
|
|
|
344
346
|
function renderImportSqlForm(modal, state) {
|
|
@@ -376,71 +378,51 @@ function renderImportSqlForm(modal, state) {
|
|
|
376
378
|
|
|
377
379
|
function renderDeleteRowConfirmForm(modal) {
|
|
378
380
|
const rowPreview = modal.rowPreview ?? [];
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
<
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
</div>
|
|
426
|
-
${renderError(modal.error)}
|
|
427
|
-
<div class="flex items-center justify-end gap-3 pt-2">
|
|
428
|
-
<button
|
|
429
|
-
class="standard-button"
|
|
430
|
-
data-action="close-modal"
|
|
431
|
-
type="button"
|
|
432
|
-
>
|
|
433
|
-
Cancel
|
|
434
|
-
</button>
|
|
435
|
-
<button
|
|
436
|
-
class="delete-button"
|
|
437
|
-
type="submit"
|
|
438
|
-
>
|
|
439
|
-
${modal.submitting ? "Deleting..." : "Delete Row"}
|
|
440
|
-
</button>
|
|
441
|
-
</div>
|
|
442
|
-
</form>
|
|
443
|
-
`;
|
|
381
|
+
const rowLabelMarkup = modal.rowLabel
|
|
382
|
+
? [
|
|
383
|
+
'<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">',
|
|
384
|
+
escapeHtml(modal.rowLabel),
|
|
385
|
+
"</div>",
|
|
386
|
+
].join("")
|
|
387
|
+
: "";
|
|
388
|
+
const rowPreviewMarkup = rowPreview.length
|
|
389
|
+
? [
|
|
390
|
+
'<div class="grid grid-cols-1 gap-3 md:grid-cols-2">',
|
|
391
|
+
rowPreview
|
|
392
|
+
.map((field) =>
|
|
393
|
+
[
|
|
394
|
+
'<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">',
|
|
395
|
+
'<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">',
|
|
396
|
+
escapeHtml(field.label),
|
|
397
|
+
"</div>",
|
|
398
|
+
'<div class="mt-2 text-sm text-on-surface" title="',
|
|
399
|
+
escapeHtml(field.fullValue ?? field.value ?? ""),
|
|
400
|
+
'">',
|
|
401
|
+
escapeHtml(field.value ?? ""),
|
|
402
|
+
"</div></div>",
|
|
403
|
+
].join("")
|
|
404
|
+
)
|
|
405
|
+
.join(""),
|
|
406
|
+
"</div>",
|
|
407
|
+
].join("")
|
|
408
|
+
: "";
|
|
409
|
+
|
|
410
|
+
return [
|
|
411
|
+
'<form class="space-y-5" data-form="delete-row-confirm"><div class="space-y-3">',
|
|
412
|
+
'<p class="text-sm leading-7 text-on-surface">Delete this row from <span class="font-bold text-primary-container">',
|
|
413
|
+
escapeHtml(modal.tableName ?? "the current table"),
|
|
414
|
+
"</span>?</p>",
|
|
415
|
+
'<p class="text-sm leading-7 text-on-surface-variant/65">This action cannot be undone.</p>',
|
|
416
|
+
rowLabelMarkup,
|
|
417
|
+
rowPreviewMarkup,
|
|
418
|
+
"</div>",
|
|
419
|
+
renderError(modal.error),
|
|
420
|
+
'<div class="flex items-center justify-end gap-3 pt-2">',
|
|
421
|
+
'<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
|
|
422
|
+
'<button class="delete-button" type="submit">',
|
|
423
|
+
modal.submitting ? "Deleting..." : "Delete Row",
|
|
424
|
+
"</button></div></form>",
|
|
425
|
+
].join("");
|
|
444
426
|
}
|
|
445
427
|
|
|
446
428
|
function renderChartColumnOptions(analysis, { allowEmpty = false, includeNumericHint = false } = {}) {
|
|
@@ -481,13 +463,22 @@ function renderChartEditorForm(modal, state) {
|
|
|
481
463
|
bind: "query-chart-draft-config:y_column",
|
|
482
464
|
})}
|
|
483
465
|
</div>
|
|
484
|
-
<div class="grid grid-cols-1 gap-4 md:grid-cols-
|
|
466
|
+
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
|
|
467
|
+
${renderSelectField({
|
|
468
|
+
label: "Sort By",
|
|
469
|
+
value: draft.config?.sort_by ?? "y",
|
|
470
|
+
options: [
|
|
471
|
+
{ value: "x", label: "X column" },
|
|
472
|
+
{ value: "y", label: "Y value" },
|
|
473
|
+
],
|
|
474
|
+
bind: "query-chart-draft-config:sort_by",
|
|
475
|
+
})}
|
|
485
476
|
${renderSelectField({
|
|
486
477
|
label: "Sort Direction",
|
|
487
|
-
value: draft.config?.sort_direction ?? "
|
|
478
|
+
value: draft.config?.sort_direction ?? "desc",
|
|
488
479
|
options: [
|
|
489
|
-
{ value: "asc", label: "Ascending" },
|
|
490
|
-
{ value: "desc", label: "Descending" },
|
|
480
|
+
{ value: "asc", label: "Ascending / smallest first" },
|
|
481
|
+
{ value: "desc", label: "Descending / largest first" },
|
|
491
482
|
],
|
|
492
483
|
bind: "query-chart-draft-config:sort_direction",
|
|
493
484
|
})}
|
|
@@ -684,69 +675,37 @@ function renderChartEditorForm(modal, state) {
|
|
|
684
675
|
}
|
|
685
676
|
|
|
686
677
|
function renderDeleteChartForm(modal) {
|
|
687
|
-
return
|
|
688
|
-
<form class="space-y-5" data-form="delete-query-chart">
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
<button
|
|
702
|
-
class="standard-button"
|
|
703
|
-
data-action="close-modal"
|
|
704
|
-
type="button"
|
|
705
|
-
>
|
|
706
|
-
Cancel
|
|
707
|
-
</button>
|
|
708
|
-
<button
|
|
709
|
-
class="delete-button"
|
|
710
|
-
type="submit"
|
|
711
|
-
>
|
|
712
|
-
${modal.submitting ? "Deleting..." : "Delete Chart"}
|
|
713
|
-
</button>
|
|
714
|
-
</div>
|
|
715
|
-
</form>
|
|
716
|
-
`;
|
|
678
|
+
return [
|
|
679
|
+
'<form class="space-y-5" data-form="delete-query-chart"><div class="space-y-3">',
|
|
680
|
+
'<p class="text-sm leading-7 text-on-surface">Delete chart <span class="font-bold text-primary-container">',
|
|
681
|
+
escapeHtml(modal.chartName ?? "Chart"),
|
|
682
|
+
"</span>?</p>",
|
|
683
|
+
'<p class="text-sm leading-7 text-on-surface-variant/65">The linked query-history entry stays intact. Only this chart definition is removed.</p>',
|
|
684
|
+
"</div>",
|
|
685
|
+
renderError(modal.error),
|
|
686
|
+
'<div class="flex items-center justify-end gap-3 pt-2">',
|
|
687
|
+
'<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
|
|
688
|
+
'<button class="delete-button" type="submit">',
|
|
689
|
+
modal.submitting ? "Deleting..." : "Delete Chart",
|
|
690
|
+
"</button></div></form>",
|
|
691
|
+
].join("");
|
|
717
692
|
}
|
|
718
693
|
|
|
719
694
|
function renderDeleteQueryHistoryForm(modal) {
|
|
720
|
-
return
|
|
721
|
-
<form class="space-y-5" data-form="delete-query-history-confirm">
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
<button
|
|
735
|
-
class="standard-button"
|
|
736
|
-
data-action="close-modal"
|
|
737
|
-
type="button"
|
|
738
|
-
>
|
|
739
|
-
Cancel
|
|
740
|
-
</button>
|
|
741
|
-
<button
|
|
742
|
-
class="delete-button"
|
|
743
|
-
type="submit"
|
|
744
|
-
>
|
|
745
|
-
${modal.submitting ? "Deleting..." : "Delete Query"}
|
|
746
|
-
</button>
|
|
747
|
-
</div>
|
|
748
|
-
</form>
|
|
749
|
-
`;
|
|
695
|
+
return [
|
|
696
|
+
'<form class="space-y-5" data-form="delete-query-history-confirm"><div class="space-y-3">',
|
|
697
|
+
'<p class="text-sm leading-7 text-on-surface">Delete query <span class="font-bold text-primary-container">',
|
|
698
|
+
escapeHtml(modal.queryTitle ?? "SQL query"),
|
|
699
|
+
"</span>?</p>",
|
|
700
|
+
'<p class="text-sm leading-7 text-on-surface-variant/65">This removes the query-history entry and all recorded runs linked to it.</p>',
|
|
701
|
+
"</div>",
|
|
702
|
+
renderError(modal.error),
|
|
703
|
+
'<div class="flex items-center justify-end gap-3 pt-2">',
|
|
704
|
+
'<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
|
|
705
|
+
'<button class="delete-button" type="submit">',
|
|
706
|
+
modal.submitting ? "Deleting..." : "Delete Query",
|
|
707
|
+
"</button></div></form>",
|
|
708
|
+
].join("");
|
|
750
709
|
}
|
|
751
710
|
|
|
752
711
|
function renderCreateMediaTaggingMappingTableForm(modal, state) {
|
|
@@ -16,7 +16,7 @@ export function renderPageHeader({ eyebrow = "", title, subtitle = "", actions =
|
|
|
16
16
|
`
|
|
17
17
|
: ""
|
|
18
18
|
}
|
|
19
|
-
<h1 class="text-5xl font-
|
|
19
|
+
<h1 class="text-5xl font-headline font-bold text-[#FCE300] tracking-tighter uppercase">${escapeHtml(
|
|
20
20
|
title
|
|
21
21
|
)}</h1>
|
|
22
22
|
${
|