sqlite-hub 0.3.2 → 0.5.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 +5 -5
- package/changelog.md +14 -0
- package/{js → frontend/js}/api.js +99 -5
- package/{js → frontend/js}/app.js +162 -11
- package/{js → frontend/js}/components/connectionCard.js +8 -9
- package/frontend/js/components/connectionLogo.js +33 -0
- package/{js → frontend/js}/components/dataGrid.js +3 -3
- package/{js → frontend/js}/components/emptyState.js +25 -11
- package/{js → frontend/js}/components/modal.js +57 -0
- package/{js → frontend/js}/components/queryEditor.js +33 -55
- package/frontend/js/components/queryHistoryDetail.js +263 -0
- package/frontend/js/components/queryHistoryPanel.js +228 -0
- package/{js → frontend/js}/components/queryResults.js +32 -46
- package/{js → frontend/js}/components/rowEditorPanel.js +73 -14
- package/{js → frontend/js}/components/sidebar.js +8 -3
- package/{js → frontend/js}/store.js +577 -21
- package/{js → frontend/js}/utils/format.js +23 -0
- package/{js → frontend/js}/views/data.js +136 -10
- package/{js → frontend/js}/views/editor.js +34 -10
- package/{js → frontend/js}/views/overview.js +15 -0
- package/{js → frontend/js}/views/structure.js +10 -12
- package/{styles → frontend/styles}/components.css +106 -0
- package/{styles → frontend/styles}/structure-graph.css +5 -10
- package/package.json +2 -2
- package/{publish_brew.sh → scripts/publish_brew.sh} +2 -2
- package/{publish_npm.sh → scripts/publish_npm.sh} +2 -2
- package/server/data/db_logos/.gitkeep +0 -0
- package/server/routes/connections.js +2 -0
- package/server/routes/data.js +2 -0
- package/server/routes/export.js +4 -1
- package/server/routes/overview.js +12 -0
- package/server/routes/sql.js +163 -5
- package/server/server.js +8 -6
- package/server/services/sqlite/connectionManager.js +68 -33
- package/server/services/sqlite/dataBrowserService.js +4 -16
- package/server/services/sqlite/exportService.js +4 -16
- package/server/services/sqlite/overviewService.js +34 -0
- package/server/services/sqlite/sqlExecutor.js +83 -63
- package/server/services/sqlite/tableSort.js +63 -0
- package/server/services/storage/appStateStore.js +832 -20
- package/server/services/storage/queryHistoryUtils.js +169 -0
- package/server/utils/appPaths.js +42 -18
- package/{assets → frontend/assets}/images/logo.webp +0 -0
- package/{assets → frontend/assets}/images/logo_extrasmall.webp +0 -0
- package/{assets → frontend/assets}/images/logo_raw.png +0 -0
- package/{assets → frontend/assets}/images/logo_small.webp +0 -0
- package/{assets → frontend/assets}/mockups/connections.png +0 -0
- package/{assets → frontend/assets}/mockups/data.png +0 -0
- package/{assets → frontend/assets}/mockups/data_edit.png +0 -0
- package/{assets → frontend/assets}/mockups/graph_visualize.png +0 -0
- package/{assets → frontend/assets}/mockups/home.png +0 -0
- package/{assets → frontend/assets}/mockups/overview.png +0 -0
- package/{assets → frontend/assets}/mockups/sql_editor.png +0 -0
- package/{assets → frontend/assets}/mockups/structure.png +0 -0
- package/{index.html → frontend/index.html} +0 -0
- package/{js → frontend/js}/components/actionBar.js +0 -0
- package/{js → frontend/js}/components/appShell.js +0 -0
- package/{js → frontend/js}/components/badges.js +0 -0
- package/{js → frontend/js}/components/bottomTabs.js +1 -1
- /package/{js → frontend/js}/components/metricCard.js +0 -0
- /package/{js → frontend/js}/components/pageHeader.js +0 -0
- /package/{js → frontend/js}/components/statusBar.js +0 -0
- /package/{js → frontend/js}/components/structureGraph.js +0 -0
- /package/{js → frontend/js}/components/toast.js +0 -0
- /package/{js → frontend/js}/components/topNav.js +0 -0
- /package/{js → frontend/js}/lib/cytoscapeRuntime.js +0 -0
- /package/{js → frontend/js}/router.js +0 -0
- /package/{js → frontend/js}/views/connections.js +0 -0
- /package/{js → frontend/js}/views/landing.js +0 -0
- /package/{js → frontend/js}/views/settings.js +0 -0
- /package/{styles → frontend/styles}/base.css +0 -0
- /package/{styles → frontend/styles}/layout.css +0 -0
- /package/{styles → frontend/styles}/tokens.css +0 -0
- /package/{styles → frontend/styles}/views.css +0 -0
- /package/{data → server/data}/.gitkeep +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { escapeHtml, truncateMiddle } from "../utils/format.js";
|
|
2
|
+
import { renderConnectionLogo } from "./connectionLogo.js";
|
|
2
3
|
|
|
3
4
|
function renderField({ label, name, type = "text", placeholder = "", value = "" }) {
|
|
4
5
|
return `
|
|
@@ -31,6 +32,32 @@ function renderCheckboxField({ label, name, checked = false, text }) {
|
|
|
31
32
|
`;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
function renderFileField({
|
|
36
|
+
label,
|
|
37
|
+
name,
|
|
38
|
+
accept = "",
|
|
39
|
+
helpText = "",
|
|
40
|
+
}) {
|
|
41
|
+
return `
|
|
42
|
+
<label class="block space-y-2">
|
|
43
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
44
|
+
${escapeHtml(label)}
|
|
45
|
+
</span>
|
|
46
|
+
<input
|
|
47
|
+
accept="${escapeHtml(accept)}"
|
|
48
|
+
class="block w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 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
|
+
name="${escapeHtml(name)}"
|
|
50
|
+
type="file"
|
|
51
|
+
/>
|
|
52
|
+
${
|
|
53
|
+
helpText
|
|
54
|
+
? `<p class="text-[11px] leading-5 text-on-surface-variant/60">${escapeHtml(helpText)}</p>`
|
|
55
|
+
: ""
|
|
56
|
+
}
|
|
57
|
+
</label>
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
|
|
34
61
|
function renderError(error) {
|
|
35
62
|
if (!error) {
|
|
36
63
|
return "";
|
|
@@ -102,6 +129,36 @@ function renderEditConnectionForm(modal) {
|
|
|
102
129
|
placeholder: "Optional display name",
|
|
103
130
|
value: connection.label ?? "",
|
|
104
131
|
})}
|
|
132
|
+
<div class="space-y-3">
|
|
133
|
+
<span class="block text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
134
|
+
Database Icon
|
|
135
|
+
</span>
|
|
136
|
+
<div class="flex flex-wrap items-center gap-4 border border-outline-variant/10 bg-surface-container-lowest px-4 py-4">
|
|
137
|
+
${renderConnectionLogo(connection, {
|
|
138
|
+
containerClass:
|
|
139
|
+
"flex h-16 w-16 items-center justify-center overflow-hidden border border-outline-variant/20 bg-surface-container-highest",
|
|
140
|
+
imageClassName: "h-full w-full object-cover",
|
|
141
|
+
iconClassName: "text-2xl text-primary-container",
|
|
142
|
+
})}
|
|
143
|
+
<div class="min-w-0 flex-1">
|
|
144
|
+
${renderFileField({
|
|
145
|
+
label: "Upload image",
|
|
146
|
+
name: "logoFile",
|
|
147
|
+
accept: ".png,.jpg,.jpeg,.webp,image/png,image/jpeg,image/webp",
|
|
148
|
+
helpText: "Allowed formats: PNG, JPG, WEBP. The file is stored in db_logos.",
|
|
149
|
+
})}
|
|
150
|
+
${
|
|
151
|
+
connection.logoUrl
|
|
152
|
+
? renderCheckboxField({
|
|
153
|
+
label: "Reset icon",
|
|
154
|
+
name: "clearLogo",
|
|
155
|
+
text: "Use the default icon again",
|
|
156
|
+
})
|
|
157
|
+
: ""
|
|
158
|
+
}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
105
162
|
${renderCheckboxField({
|
|
106
163
|
label: "Open read-only",
|
|
107
164
|
name: "readOnly",
|
|
@@ -9,23 +9,6 @@ function renderLineNumbers(query) {
|
|
|
9
9
|
.join("");
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function renderHistoryOptions(history) {
|
|
13
|
-
if (!history.length) {
|
|
14
|
-
return '<option value="">No recent statements</option>';
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return [
|
|
18
|
-
'<option value="">Load recent statement...</option>',
|
|
19
|
-
...history.map(
|
|
20
|
-
(entry) => `
|
|
21
|
-
<option value="${escapeHtml(entry.id)}">
|
|
22
|
-
${escapeHtml(entry.sql.replace(/\s+/g, " ").slice(0, 96))}
|
|
23
|
-
</option>
|
|
24
|
-
`
|
|
25
|
-
),
|
|
26
|
-
].join("");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
12
|
function renderHighlightedQuery(query) {
|
|
30
13
|
if (query) {
|
|
31
14
|
return highlightSql(query);
|
|
@@ -34,13 +17,41 @@ function renderHighlightedQuery(query) {
|
|
|
34
17
|
return '<span class="text-on-surface-variant/35">SELECT name FROM sqlite_master WHERE type = \'table\';</span>';
|
|
35
18
|
}
|
|
36
19
|
|
|
20
|
+
function renderEditorSurface({ query }) {
|
|
21
|
+
return `
|
|
22
|
+
<div class="flex min-h-0 flex-1 overflow-hidden">
|
|
23
|
+
<div class="flex w-12 flex-col items-center bg-surface-container-lowest py-4 font-mono text-xs select-none text-outline-variant/30">
|
|
24
|
+
${renderLineNumbers(query)}
|
|
25
|
+
</div>
|
|
26
|
+
<div class="relative flex-1 overflow-hidden bg-surface-container p-6 font-mono text-sm leading-relaxed">
|
|
27
|
+
<div class="pointer-events-none absolute right-0 top-0 p-4 opacity-5">
|
|
28
|
+
<span class="material-symbols-outlined text-[120px] font-thin">terminal</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="query-editor-layer relative z-10 h-full min-h-[140px]">
|
|
31
|
+
<pre
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
class="query-editor-highlight"
|
|
34
|
+
data-query-editor-highlight
|
|
35
|
+
>${renderHighlightedQuery(query)}</pre>
|
|
36
|
+
<textarea
|
|
37
|
+
class="query-editor-input custom-scrollbar relative z-10 h-full min-h-[140px] w-full resize-none border-none focus:ring-0"
|
|
38
|
+
data-bind="current-query"
|
|
39
|
+
placeholder="SELECT name FROM sqlite_master WHERE type = 'table';"
|
|
40
|
+
spellcheck="false"
|
|
41
|
+
>${escapeHtml(query)}</textarea>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
export function renderQueryEditor({
|
|
38
49
|
query,
|
|
39
50
|
title,
|
|
40
51
|
executing = false,
|
|
41
52
|
exporting = false,
|
|
42
|
-
history = [],
|
|
43
53
|
historyLoading = false,
|
|
54
|
+
historyTotal = 0,
|
|
44
55
|
}) {
|
|
45
56
|
const left = `
|
|
46
57
|
<div class="flex items-center gap-2 bg-surface-container-lowest px-3 py-1">
|
|
@@ -51,24 +62,11 @@ export function renderQueryEditor({
|
|
|
51
62
|
</div>
|
|
52
63
|
<div class="hidden items-center gap-2 text-[10px] font-mono uppercase tracking-widest text-on-surface-variant/40 md:flex">
|
|
53
64
|
<span class="material-symbols-outlined text-xs">history</span>
|
|
54
|
-
${historyLoading ? "Loading history..." : `${
|
|
65
|
+
${historyLoading ? "Loading history..." : `${historyTotal} queries tracked`}
|
|
55
66
|
</div>
|
|
56
67
|
`;
|
|
57
68
|
|
|
58
69
|
const right = `
|
|
59
|
-
<select
|
|
60
|
-
class="min-w-[220px] border border-outline-variant/20 bg-surface-container-lowest px-3 py-2 text-[10px] font-mono uppercase tracking-[0.14em] text-on-surface-variant outline-none"
|
|
61
|
-
data-bind="history-entry"
|
|
62
|
-
>
|
|
63
|
-
${renderHistoryOptions(history)}
|
|
64
|
-
</select>
|
|
65
|
-
<button
|
|
66
|
-
class="px-4 py-1.5 text-[10px] font-bold uppercase tracking-widest text-on-surface hover:bg-surface-container-highest transition-colors"
|
|
67
|
-
data-action="clear-sql-history"
|
|
68
|
-
type="button"
|
|
69
|
-
>
|
|
70
|
-
Clear History
|
|
71
|
-
</button>
|
|
72
70
|
<button
|
|
73
71
|
class="px-4 py-1.5 text-[10px] font-bold uppercase tracking-widest text-on-surface hover:bg-surface-container-highest transition-colors"
|
|
74
72
|
data-action="clear-query"
|
|
@@ -93,7 +91,7 @@ export function renderQueryEditor({
|
|
|
93
91
|
`;
|
|
94
92
|
|
|
95
93
|
return `
|
|
96
|
-
<div class="flex h-full flex-col">
|
|
94
|
+
<div class="flex h-full min-h-0 flex-col">
|
|
97
95
|
<div class="bg-surface-container-low px-6 py-3">
|
|
98
96
|
${renderActionBar({
|
|
99
97
|
left,
|
|
@@ -101,28 +99,8 @@ export function renderQueryEditor({
|
|
|
101
99
|
className: "flex-wrap",
|
|
102
100
|
})}
|
|
103
101
|
</div>
|
|
104
|
-
<div class="flex flex-1
|
|
105
|
-
|
|
106
|
-
${renderLineNumbers(query)}
|
|
107
|
-
</div>
|
|
108
|
-
<div class="relative flex-1 overflow-hidden bg-surface-container p-6 font-mono text-sm leading-relaxed">
|
|
109
|
-
<div class="pointer-events-none absolute right-0 top-0 p-4 opacity-5">
|
|
110
|
-
<span class="material-symbols-outlined text-[120px] font-thin">terminal</span>
|
|
111
|
-
</div>
|
|
112
|
-
<div class="query-editor-layer relative z-10 h-full min-h-[140px]">
|
|
113
|
-
<pre
|
|
114
|
-
aria-hidden="true"
|
|
115
|
-
class="query-editor-highlight"
|
|
116
|
-
data-query-editor-highlight
|
|
117
|
-
>${renderHighlightedQuery(query)}</pre>
|
|
118
|
-
<textarea
|
|
119
|
-
class="query-editor-input custom-scrollbar relative z-10 h-full min-h-[140px] w-full resize-none border-none focus:ring-0"
|
|
120
|
-
data-bind="current-query"
|
|
121
|
-
placeholder="SELECT name FROM sqlite_master WHERE type = 'table';"
|
|
122
|
-
spellcheck="false"
|
|
123
|
-
>${escapeHtml(query)}</textarea>
|
|
124
|
-
</div>
|
|
125
|
-
</div>
|
|
102
|
+
<div class="flex min-h-0 flex-1 flex-col">
|
|
103
|
+
${renderEditorSurface({ query })}
|
|
126
104
|
</div>
|
|
127
105
|
</div>
|
|
128
106
|
`;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import {
|
|
2
|
+
escapeHtml,
|
|
3
|
+
formatCompactDateTime,
|
|
4
|
+
formatDateTime,
|
|
5
|
+
formatDurationMs,
|
|
6
|
+
formatNumber,
|
|
7
|
+
highlightSql,
|
|
8
|
+
} from "../utils/format.js";
|
|
9
|
+
import { renderStatusBadge } from "./badges.js";
|
|
10
|
+
|
|
11
|
+
function renderDetailMetaItem(label, value) {
|
|
12
|
+
return `
|
|
13
|
+
<div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
|
|
14
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
15
|
+
${escapeHtml(label)}
|
|
16
|
+
</div>
|
|
17
|
+
<div class="mt-2 text-sm text-on-surface">${escapeHtml(value)}</div>
|
|
18
|
+
</div>
|
|
19
|
+
`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function renderRunItem(run) {
|
|
23
|
+
return `
|
|
24
|
+
<div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
|
|
25
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
26
|
+
${renderStatusBadge(run.status, run.status === "error" ? "alert" : "success")}
|
|
27
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
28
|
+
${escapeHtml(formatCompactDateTime(run.executedAt))}
|
|
29
|
+
</span>
|
|
30
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
31
|
+
${escapeHtml(formatDurationMs(run.durationMs))}
|
|
32
|
+
</span>
|
|
33
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
34
|
+
rows ${escapeHtml(formatNumber(run.rowCount ?? 0))}
|
|
35
|
+
</span>
|
|
36
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
37
|
+
affected ${escapeHtml(formatNumber(run.affectedRows ?? 0))}
|
|
38
|
+
</span>
|
|
39
|
+
</div>
|
|
40
|
+
${
|
|
41
|
+
run.errorMessage
|
|
42
|
+
? `
|
|
43
|
+
<div class="mt-3 border border-error/20 bg-error-container/20 px-3 py-3 text-sm leading-6 text-error">
|
|
44
|
+
${escapeHtml(run.errorMessage)}
|
|
45
|
+
</div>
|
|
46
|
+
`
|
|
47
|
+
: ""
|
|
48
|
+
}
|
|
49
|
+
</div>
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function renderQueryHistoryDetail({
|
|
54
|
+
item = null,
|
|
55
|
+
runs = [],
|
|
56
|
+
loading = false,
|
|
57
|
+
error = null,
|
|
58
|
+
}) {
|
|
59
|
+
if (!item && !loading && !error) {
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (loading) {
|
|
64
|
+
return `
|
|
65
|
+
<section class="flex h-full min-h-0 flex-col bg-surface-low">
|
|
66
|
+
<div class="border-b border-outline-variant/10 px-5 py-4">
|
|
67
|
+
<div class="flex items-center justify-between gap-3">
|
|
68
|
+
<span class="font-headline text-sm font-black uppercase tracking-[0.18em] text-primary-container">
|
|
69
|
+
Query Detail
|
|
70
|
+
</span>
|
|
71
|
+
<button
|
|
72
|
+
class="query-history-icon-button"
|
|
73
|
+
data-action="clear-query-history-selection"
|
|
74
|
+
type="button"
|
|
75
|
+
>
|
|
76
|
+
<span class="material-symbols-outlined text-[18px]">close</span>
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="flex flex-1 items-center justify-center px-6 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
81
|
+
Loading query detail...
|
|
82
|
+
</div>
|
|
83
|
+
</section>
|
|
84
|
+
`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (error) {
|
|
88
|
+
return `
|
|
89
|
+
<section class="flex h-full min-h-0 flex-col bg-surface-low">
|
|
90
|
+
<div class="border-b border-outline-variant/10 px-5 py-4">
|
|
91
|
+
<div class="flex items-center justify-between gap-3">
|
|
92
|
+
<span class="font-headline text-sm font-black uppercase tracking-[0.18em] text-primary-container">
|
|
93
|
+
Query Detail
|
|
94
|
+
</span>
|
|
95
|
+
<button
|
|
96
|
+
class="query-history-icon-button"
|
|
97
|
+
data-action="clear-query-history-selection"
|
|
98
|
+
type="button"
|
|
99
|
+
>
|
|
100
|
+
<span class="material-symbols-outlined text-[18px]">close</span>
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="p-5">
|
|
105
|
+
<div class="border border-error/30 bg-error-container/20 px-4 py-4 text-sm text-error">
|
|
106
|
+
${escapeHtml(error.message)}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</section>
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return `
|
|
114
|
+
<section class="flex h-full min-h-0 flex-col bg-surface-low">
|
|
115
|
+
<div class="border-b border-outline-variant/10 px-5 py-4">
|
|
116
|
+
<div class="flex items-center justify-between gap-3">
|
|
117
|
+
<div>
|
|
118
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/70">
|
|
119
|
+
Query Detail
|
|
120
|
+
</div>
|
|
121
|
+
<h2 class="mt-1 font-headline text-lg font-black uppercase tracking-tight text-on-surface">
|
|
122
|
+
${escapeHtml(item.displayTitle)}
|
|
123
|
+
</h2>
|
|
124
|
+
</div>
|
|
125
|
+
<button
|
|
126
|
+
class="query-history-icon-button"
|
|
127
|
+
data-action="clear-query-history-selection"
|
|
128
|
+
type="button"
|
|
129
|
+
>
|
|
130
|
+
<span class="material-symbols-outlined text-[18px]">close</span>
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="mt-4 flex flex-wrap gap-2">
|
|
134
|
+
${renderStatusBadge(item.queryType, item.isDestructive ? "alert" : "primary")}
|
|
135
|
+
${item.isSaved ? renderStatusBadge("saved", "primary") : ""}
|
|
136
|
+
${item.isDestructive ? renderStatusBadge("destructive", "alert") : ""}
|
|
137
|
+
${item.lastRun ? renderStatusBadge(item.lastRun.status, item.lastRun.status === "error" ? "alert" : "success") : ""}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="custom-scrollbar min-h-0 flex-1 overflow-auto px-5 py-5">
|
|
141
|
+
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
142
|
+
${renderDetailMetaItem("Last Used", formatDateTime(item.lastUsedAt))}
|
|
143
|
+
${renderDetailMetaItem("First Executed", formatDateTime(item.firstExecutedAt))}
|
|
144
|
+
${renderDetailMetaItem("Use Count", formatNumber(item.useCount))}
|
|
145
|
+
${renderDetailMetaItem(
|
|
146
|
+
"Tables",
|
|
147
|
+
item.tablesDetected?.length ? item.tablesDetected.join(", ") : "None detected"
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<form class="mt-5" data-form="save-query-history-title">
|
|
152
|
+
<input name="historyId" type="hidden" value="${escapeHtml(item.id)}" />
|
|
153
|
+
<label class="block text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
154
|
+
Custom Title
|
|
155
|
+
</label>
|
|
156
|
+
<div class="mt-2 flex gap-2">
|
|
157
|
+
<input
|
|
158
|
+
class="flex-1 border border-outline-variant/20 bg-surface-container px-3 py-2 text-sm text-on-surface outline-none placeholder:text-on-surface-variant/35 focus:border-primary-container"
|
|
159
|
+
name="title"
|
|
160
|
+
placeholder="${escapeHtml(item.displayTitle)}"
|
|
161
|
+
type="text"
|
|
162
|
+
value="${escapeHtml(item.title ?? "")}"
|
|
163
|
+
/>
|
|
164
|
+
<button
|
|
165
|
+
class="border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
|
|
166
|
+
type="submit"
|
|
167
|
+
>
|
|
168
|
+
Save
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
<p class="mt-2 text-xs text-on-surface-variant/60">
|
|
172
|
+
Leave empty to fall back to the auto title generated from the query.
|
|
173
|
+
</p>
|
|
174
|
+
</form>
|
|
175
|
+
|
|
176
|
+
<div class="mt-5 flex flex-wrap gap-2">
|
|
177
|
+
<button
|
|
178
|
+
class="toolbar-button border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
|
|
179
|
+
data-action="open-query-history"
|
|
180
|
+
data-history-id="${escapeHtml(item.id)}"
|
|
181
|
+
type="button"
|
|
182
|
+
>
|
|
183
|
+
Open In Editor
|
|
184
|
+
</button>
|
|
185
|
+
<button
|
|
186
|
+
class="toolbar-button border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
|
|
187
|
+
data-action="run-query-history"
|
|
188
|
+
data-history-id="${escapeHtml(item.id)}"
|
|
189
|
+
type="button"
|
|
190
|
+
>
|
|
191
|
+
Run Now
|
|
192
|
+
</button>
|
|
193
|
+
<button
|
|
194
|
+
class="toolbar-button border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
|
|
195
|
+
data-action="toggle-query-history-saved"
|
|
196
|
+
data-history-id="${escapeHtml(item.id)}"
|
|
197
|
+
data-next-value="${item.isSaved ? "false" : "true"}"
|
|
198
|
+
type="button"
|
|
199
|
+
>
|
|
200
|
+
${item.isSaved ? "Unsave" : "Save"}
|
|
201
|
+
</button>
|
|
202
|
+
<button
|
|
203
|
+
class="toolbar-button border border-error/25 bg-error-container/20 px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-error transition-colors hover:border-error hover:bg-error-container/30"
|
|
204
|
+
data-action="delete-query-history"
|
|
205
|
+
data-history-id="${escapeHtml(item.id)}"
|
|
206
|
+
type="button"
|
|
207
|
+
>
|
|
208
|
+
Delete
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div class="mt-6">
|
|
213
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
214
|
+
SQL
|
|
215
|
+
</div>
|
|
216
|
+
<pre class="query-history-detail-sql custom-scrollbar mt-2 overflow-auto border border-outline-variant/10 bg-surface-container-lowest p-4 font-mono text-sm leading-6 text-on-surface"><code>${highlightSql(
|
|
217
|
+
item.rawSql
|
|
218
|
+
)}</code></pre>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<form class="mt-6" data-form="save-query-history-notes">
|
|
222
|
+
<input name="historyId" type="hidden" value="${escapeHtml(item.id)}" />
|
|
223
|
+
<label class="block text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
224
|
+
Notes
|
|
225
|
+
</label>
|
|
226
|
+
<textarea class="custom-scrollbar mt-2 min-h-[120px] w-full resize-y border border-outline-variant/20 bg-surface-container px-3 py-3 text-sm leading-6 text-on-surface outline-none placeholder:text-on-surface-variant/35 focus:border-primary-container" name="notes" placeholder="Add context, caveats, or why this query matters...">${escapeHtml(
|
|
227
|
+
item.notes ?? ""
|
|
228
|
+
)}</textarea>
|
|
229
|
+
<div class="mt-2 flex justify-end">
|
|
230
|
+
<button
|
|
231
|
+
class="border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
|
|
232
|
+
type="submit"
|
|
233
|
+
>
|
|
234
|
+
Save Notes
|
|
235
|
+
</button>
|
|
236
|
+
</div>
|
|
237
|
+
</form>
|
|
238
|
+
|
|
239
|
+
<div class="mt-6">
|
|
240
|
+
<div class="flex items-center justify-between gap-3">
|
|
241
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
242
|
+
Latest Runs
|
|
243
|
+
</div>
|
|
244
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/40">
|
|
245
|
+
${escapeHtml(formatNumber(runs.length))}
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
<div class="mt-3 space-y-3">
|
|
249
|
+
${
|
|
250
|
+
runs.length
|
|
251
|
+
? runs.map((run) => renderRunItem(run)).join("")
|
|
252
|
+
: `
|
|
253
|
+
<div class="border border-outline-variant/10 bg-surface-container px-3 py-4 text-sm text-on-surface-variant/65">
|
|
254
|
+
No execution runs recorded yet.
|
|
255
|
+
</div>
|
|
256
|
+
`
|
|
257
|
+
}
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
</section>
|
|
262
|
+
`;
|
|
263
|
+
}
|