sqlite-hub 0.4.0 → 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/changelog.md +7 -0
- package/frontend/js/api.js +99 -5
- package/frontend/js/app.js +90 -11
- package/frontend/js/components/bottomTabs.js +1 -1
- package/frontend/js/components/dataGrid.js +3 -3
- package/frontend/js/components/queryEditor.js +33 -55
- package/frontend/js/components/queryHistoryDetail.js +263 -0
- package/frontend/js/components/queryHistoryPanel.js +228 -0
- package/frontend/js/components/queryResults.js +32 -46
- package/frontend/js/components/rowEditorPanel.js +73 -14
- package/frontend/js/store.js +545 -21
- package/frontend/js/utils/format.js +23 -0
- package/frontend/js/views/data.js +34 -1
- package/frontend/js/views/editor.js +34 -10
- package/frontend/js/views/overview.js +15 -0
- package/frontend/styles/components.css +106 -0
- package/package.json +1 -1
- 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 +1 -1
- 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 +674 -1
- package/server/services/storage/queryHistoryUtils.js +169 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import {
|
|
2
|
+
escapeHtml,
|
|
3
|
+
formatNumber,
|
|
4
|
+
} from "../utils/format.js";
|
|
5
|
+
import { renderStatusBadge } from "./badges.js";
|
|
6
|
+
|
|
7
|
+
function getQueryTypeTone(queryType, isDestructive) {
|
|
8
|
+
if (isDestructive) {
|
|
9
|
+
return "alert";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (queryType === "select") {
|
|
13
|
+
return "success";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (queryType === "pragma") {
|
|
17
|
+
return "primary";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return "muted";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function renderHistoryItem(item, activeHistoryId, selectedHistoryId) {
|
|
24
|
+
const isActive = Number(activeHistoryId) === Number(item.id);
|
|
25
|
+
const isSelected = Number(selectedHistoryId) === Number(item.id);
|
|
26
|
+
const visibleTables = (item.tablesDetected ?? []).slice(0, 3);
|
|
27
|
+
|
|
28
|
+
return `
|
|
29
|
+
<article class="query-history-item ${isActive ? "is-active" : ""} ${
|
|
30
|
+
item.lastRun?.status === "error" ? "is-error" : ""
|
|
31
|
+
}">
|
|
32
|
+
<button
|
|
33
|
+
class="query-history-item-hit ${isActive ? "is-active" : ""}"
|
|
34
|
+
data-action="select-query-history-item"
|
|
35
|
+
data-history-id="${escapeHtml(item.id)}"
|
|
36
|
+
type="button"
|
|
37
|
+
>
|
|
38
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
39
|
+
<span class="truncate font-headline text-sm font-bold uppercase tracking-tight text-on-surface">
|
|
40
|
+
${escapeHtml(item.displayTitle)}
|
|
41
|
+
</span>
|
|
42
|
+
${renderStatusBadge(item.queryType, getQueryTypeTone(item.queryType, item.isDestructive))}
|
|
43
|
+
${item.isSaved ? renderStatusBadge("saved", "primary") : ""}
|
|
44
|
+
${item.isDestructive ? renderStatusBadge("destructive", "alert") : ""}
|
|
45
|
+
</div>
|
|
46
|
+
<p class="query-history-sql-preview mt-2 text-left font-mono text-xs leading-5 text-on-surface-variant/75">
|
|
47
|
+
${escapeHtml(item.previewSql)}
|
|
48
|
+
</p>
|
|
49
|
+
</button>
|
|
50
|
+
<div class="flex items-center justify-between gap-3 border-t border-outline-variant/10 px-3 pb-3 pt-2">
|
|
51
|
+
<div class="min-w-0 flex flex-wrap gap-2 text-[10px] font-mono uppercase tracking-[0.14em] text-on-surface-variant/55">
|
|
52
|
+
${visibleTables
|
|
53
|
+
.map(
|
|
54
|
+
(tableName) => `
|
|
55
|
+
<span class="border border-outline-variant/20 bg-surface-highest px-2 py-1">
|
|
56
|
+
${escapeHtml(tableName)}
|
|
57
|
+
</span>
|
|
58
|
+
`
|
|
59
|
+
)
|
|
60
|
+
.join("")}
|
|
61
|
+
</div>
|
|
62
|
+
<div class="flex items-center gap-1">
|
|
63
|
+
<button
|
|
64
|
+
class="query-history-icon-button"
|
|
65
|
+
data-action="open-query-history"
|
|
66
|
+
data-history-id="${escapeHtml(item.id)}"
|
|
67
|
+
title="Open in editor"
|
|
68
|
+
type="button"
|
|
69
|
+
>
|
|
70
|
+
<span class="material-symbols-outlined text-[18px]">edit_note</span>
|
|
71
|
+
</button>
|
|
72
|
+
<button
|
|
73
|
+
class="query-history-icon-button"
|
|
74
|
+
data-action="run-query-history"
|
|
75
|
+
data-history-id="${escapeHtml(item.id)}"
|
|
76
|
+
title="Run query"
|
|
77
|
+
type="button"
|
|
78
|
+
>
|
|
79
|
+
<span class="material-symbols-outlined text-[18px]">play_arrow</span>
|
|
80
|
+
</button>
|
|
81
|
+
<button
|
|
82
|
+
class="query-history-icon-button ${item.isSaved ? "is-active" : ""}"
|
|
83
|
+
data-action="toggle-query-history-saved"
|
|
84
|
+
data-history-id="${escapeHtml(item.id)}"
|
|
85
|
+
data-next-value="${item.isSaved ? "false" : "true"}"
|
|
86
|
+
title="${item.isSaved ? "Remove from saved" : "Save query"}"
|
|
87
|
+
type="button"
|
|
88
|
+
>
|
|
89
|
+
<span class="material-symbols-outlined text-[18px]">
|
|
90
|
+
${item.isSaved ? "bookmark" : "bookmark_add"}
|
|
91
|
+
</span>
|
|
92
|
+
</button>
|
|
93
|
+
<button
|
|
94
|
+
class="query-history-icon-button ${isSelected ? "is-active" : ""}"
|
|
95
|
+
data-action="select-query-history-item"
|
|
96
|
+
data-history-id="${escapeHtml(item.id)}"
|
|
97
|
+
title="Open query detail"
|
|
98
|
+
type="button"
|
|
99
|
+
>
|
|
100
|
+
<span class="material-symbols-outlined text-[18px]">info</span>
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</article>
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function renderQueryHistoryTabs(activeTab, historyTotal) {
|
|
109
|
+
const tabs = [
|
|
110
|
+
{ id: "recent", label: "Recent" },
|
|
111
|
+
{ id: "saved", label: "Saved" },
|
|
112
|
+
{ id: "failed", label: "Failed" },
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
return `
|
|
116
|
+
<div class="flex items-center gap-2">
|
|
117
|
+
${tabs
|
|
118
|
+
.map(
|
|
119
|
+
(tab) => `
|
|
120
|
+
<button
|
|
121
|
+
class="query-history-tab ${activeTab === tab.id ? "is-active" : ""}"
|
|
122
|
+
data-action="set-query-history-tab"
|
|
123
|
+
data-tab="${tab.id}"
|
|
124
|
+
type="button"
|
|
125
|
+
>
|
|
126
|
+
${escapeHtml(tab.label)}
|
|
127
|
+
</button>
|
|
128
|
+
`
|
|
129
|
+
)
|
|
130
|
+
.join("")}
|
|
131
|
+
<span class="ml-auto text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/50">
|
|
132
|
+
${escapeHtml(formatNumber(historyTotal))}
|
|
133
|
+
</span>
|
|
134
|
+
</div>
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function renderQueryHistoryPanel({
|
|
139
|
+
items = [],
|
|
140
|
+
loading = false,
|
|
141
|
+
loadingMore = false,
|
|
142
|
+
error = null,
|
|
143
|
+
activeTab = "recent",
|
|
144
|
+
search = "",
|
|
145
|
+
total = 0,
|
|
146
|
+
hasMore = false,
|
|
147
|
+
activeHistoryId = null,
|
|
148
|
+
selectedHistoryId = null,
|
|
149
|
+
}) {
|
|
150
|
+
return `
|
|
151
|
+
<aside class="query-history-panel border-l border-outline-variant/10 bg-surface-container-lowest">
|
|
152
|
+
<div class="border-b border-outline-variant/10 px-4 py-4">
|
|
153
|
+
<div class="flex items-center gap-2">
|
|
154
|
+
<span class="material-symbols-outlined text-[18px] text-primary-container">history</span>
|
|
155
|
+
<span class="font-headline text-xs font-black uppercase tracking-[0.18em] text-primary-container">
|
|
156
|
+
Query History
|
|
157
|
+
</span>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="mt-4">${renderQueryHistoryTabs(activeTab, total)}</div>
|
|
160
|
+
<label class="mt-4 block">
|
|
161
|
+
<span class="sr-only">Search query history</span>
|
|
162
|
+
<input
|
|
163
|
+
class="w-full 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"
|
|
164
|
+
data-bind="query-history-search"
|
|
165
|
+
placeholder="Search SQL, titles, notes..."
|
|
166
|
+
type="search"
|
|
167
|
+
value="${escapeHtml(search)}"
|
|
168
|
+
/>
|
|
169
|
+
</label>
|
|
170
|
+
</div>
|
|
171
|
+
<div class="custom-scrollbar min-h-0 flex-1 overflow-auto px-3 py-3">
|
|
172
|
+
${
|
|
173
|
+
error
|
|
174
|
+
? `
|
|
175
|
+
<div class="border border-error/30 bg-error-container/20 px-4 py-3 text-sm text-error">
|
|
176
|
+
${escapeHtml(error.message)}
|
|
177
|
+
</div>
|
|
178
|
+
`
|
|
179
|
+
: ""
|
|
180
|
+
}
|
|
181
|
+
${
|
|
182
|
+
!loading && !items.length
|
|
183
|
+
? `
|
|
184
|
+
<div class="flex h-full min-h-[240px] flex-col items-center justify-center px-6 text-center">
|
|
185
|
+
<span class="material-symbols-outlined text-4xl text-on-surface-variant/25">manage_search</span>
|
|
186
|
+
<p class="mt-4 font-headline text-lg font-bold uppercase tracking-tight text-on-surface">
|
|
187
|
+
No Matching Queries
|
|
188
|
+
</p>
|
|
189
|
+
<p class="mt-2 max-w-xs text-sm leading-6 text-on-surface-variant/65">
|
|
190
|
+
Executed statements will appear here once they run against the active database.
|
|
191
|
+
</p>
|
|
192
|
+
</div>
|
|
193
|
+
`
|
|
194
|
+
: ""
|
|
195
|
+
}
|
|
196
|
+
<div class="space-y-3">
|
|
197
|
+
${items
|
|
198
|
+
.map((item) => renderHistoryItem(item, activeHistoryId, selectedHistoryId))
|
|
199
|
+
.join("")}
|
|
200
|
+
</div>
|
|
201
|
+
${
|
|
202
|
+
loading
|
|
203
|
+
? `
|
|
204
|
+
<div class="mt-4 text-center text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/50">
|
|
205
|
+
Loading query history...
|
|
206
|
+
</div>
|
|
207
|
+
`
|
|
208
|
+
: ""
|
|
209
|
+
}
|
|
210
|
+
${
|
|
211
|
+
hasMore
|
|
212
|
+
? `
|
|
213
|
+
<div class="mt-4 flex justify-center">
|
|
214
|
+
<button
|
|
215
|
+
class="border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant transition-colors hover:border-primary-container hover:text-primary-container"
|
|
216
|
+
data-action="load-more-query-history"
|
|
217
|
+
type="button"
|
|
218
|
+
>
|
|
219
|
+
${loadingMore ? "Loading..." : "Load More"}
|
|
220
|
+
</button>
|
|
221
|
+
</div>
|
|
222
|
+
`
|
|
223
|
+
: ""
|
|
224
|
+
}
|
|
225
|
+
</div>
|
|
226
|
+
</aside>
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
@@ -1,9 +1,39 @@
|
|
|
1
1
|
import { renderDataGrid } from "./dataGrid.js";
|
|
2
2
|
import { escapeHtml, formatCellValue, formatNumber } from "../utils/format.js";
|
|
3
3
|
|
|
4
|
+
function getSortIcon(columnName, sortColumn, sortDirection) {
|
|
5
|
+
if (columnName !== sortColumn) {
|
|
6
|
+
return "unfold_more";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return sortDirection === "desc" ? "south" : "north";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function renderSortableHeader(columnName, sortColumn, sortDirection) {
|
|
13
|
+
const isActive = columnName === sortColumn;
|
|
14
|
+
|
|
15
|
+
return `
|
|
16
|
+
<button
|
|
17
|
+
class="flex w-full items-center justify-between gap-2 text-left transition-colors ${
|
|
18
|
+
isActive ? "text-primary-container" : "text-on-surface-variant hover:text-primary-container"
|
|
19
|
+
}"
|
|
20
|
+
data-action="sort-editor-results-column"
|
|
21
|
+
data-column-name="${escapeHtml(columnName)}"
|
|
22
|
+
type="button"
|
|
23
|
+
>
|
|
24
|
+
<span class="truncate">${escapeHtml(columnName)}</span>
|
|
25
|
+
<span class="material-symbols-outlined text-sm leading-none">${getSortIcon(
|
|
26
|
+
columnName,
|
|
27
|
+
sortColumn,
|
|
28
|
+
sortDirection
|
|
29
|
+
)}</span>
|
|
30
|
+
</button>
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
|
|
4
34
|
export function renderQueryResultsPane(
|
|
5
35
|
result,
|
|
6
|
-
{
|
|
36
|
+
{ selectedRowIndex = null, editable = false, sortColumn = null, sortDirection = null } = {}
|
|
7
37
|
) {
|
|
8
38
|
if (!result) {
|
|
9
39
|
return `
|
|
@@ -17,9 +47,9 @@ export function renderQueryResultsPane(
|
|
|
17
47
|
}
|
|
18
48
|
|
|
19
49
|
const columns = (result.columns ?? []).map((columnName) => ({
|
|
20
|
-
label: escapeHtml(columnName),
|
|
21
50
|
headerClassName:
|
|
22
51
|
"border-b-2 border-primary-container px-4 py-3 text-[10px] font-bold uppercase tracking-widest",
|
|
52
|
+
renderHeader: () => renderSortableHeader(columnName, sortColumn, sortDirection),
|
|
23
53
|
cellClassName: "px-4 py-3 align-top text-on-surface",
|
|
24
54
|
render: (row) => {
|
|
25
55
|
const value = formatCellValue(row[columnName]);
|
|
@@ -32,50 +62,6 @@ export function renderQueryResultsPane(
|
|
|
32
62
|
|
|
33
63
|
return `
|
|
34
64
|
<div class="relative flex h-full min-h-0 flex-col overflow-hidden bg-surface-container">
|
|
35
|
-
<div class="flex items-center justify-between gap-4 bg-surface-container-high px-4 py-2">
|
|
36
|
-
<div class="flex items-center gap-4">
|
|
37
|
-
<div class="flex items-center gap-2 text-primary-container">
|
|
38
|
-
<span class="material-symbols-outlined text-sm">database</span>
|
|
39
|
-
<span class="text-[10px] font-bold uppercase tracking-widest">Query Results</span>
|
|
40
|
-
</div>
|
|
41
|
-
<div class="h-3 w-px bg-outline-variant/30"></div>
|
|
42
|
-
<div class="text-[10px] font-mono text-on-surface-variant/60">
|
|
43
|
-
ROWS_RETURNED: ${escapeHtml(formatNumber(result.rows?.length ?? 0))}
|
|
44
|
-
</div>
|
|
45
|
-
<div class="text-[10px] font-mono text-on-surface-variant/60">
|
|
46
|
-
AFFECTED: ${escapeHtml(formatNumber(result.affectedRowCount ?? 0))}
|
|
47
|
-
</div>
|
|
48
|
-
<div class="text-[10px] font-mono text-on-surface-variant/60">
|
|
49
|
-
EXEC: ${escapeHtml(String(result.timingMs ?? 0))}ms
|
|
50
|
-
</div>
|
|
51
|
-
${
|
|
52
|
-
editStatusMessage
|
|
53
|
-
? `
|
|
54
|
-
<div class="h-3 w-px bg-outline-variant/30"></div>
|
|
55
|
-
<div class="text-[10px] font-mono text-on-surface-variant/60">
|
|
56
|
-
${escapeHtml(editStatusMessage)}
|
|
57
|
-
</div>
|
|
58
|
-
`
|
|
59
|
-
: ""
|
|
60
|
-
}
|
|
61
|
-
</div>
|
|
62
|
-
<div class="flex gap-4 text-[10px] font-mono uppercase">
|
|
63
|
-
<button
|
|
64
|
-
class="text-on-surface-variant transition-colors hover:text-primary-container"
|
|
65
|
-
data-action="export-query-csv"
|
|
66
|
-
type="button"
|
|
67
|
-
>
|
|
68
|
-
${exporting ? "Exporting..." : "Export CSV"}
|
|
69
|
-
</button>
|
|
70
|
-
<button
|
|
71
|
-
class="text-on-surface-variant transition-colors hover:text-primary-container"
|
|
72
|
-
data-action="clear-results"
|
|
73
|
-
type="button"
|
|
74
|
-
>
|
|
75
|
-
Clear Results
|
|
76
|
-
</button>
|
|
77
|
-
</div>
|
|
78
|
-
</div>
|
|
79
65
|
<div class="custom-scrollbar min-h-0 flex-1 overflow-auto bg-surface-container-lowest">
|
|
80
66
|
${
|
|
81
67
|
result.columns?.length
|