sqlite-hub 0.9.3 → 0.9.6

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.
Files changed (48) hide show
  1. package/.github/workflows/ci.yml +36 -0
  2. package/README.md +2 -2
  3. package/bin/sqlite-hub.js +1 -1
  4. package/frontend/index.html +2 -158
  5. package/frontend/js/app.js +41 -22
  6. package/frontend/js/components/connectionCard.js +62 -87
  7. package/frontend/js/components/emptyState.js +20 -23
  8. package/frontend/js/components/modal.js +145 -195
  9. package/frontend/js/components/pageHeader.js +1 -1
  10. package/frontend/js/components/queryEditor.js +16 -30
  11. package/frontend/js/components/queryHistoryDetail.js +93 -164
  12. package/frontend/js/components/queryHistoryPanel.js +81 -99
  13. package/frontend/js/components/queryResults.js +3 -1
  14. package/frontend/js/components/rowEditorPanel.js +28 -31
  15. package/frontend/js/components/structureGraph.js +10 -9
  16. package/frontend/js/components/tableDesignerEditor.js +91 -116
  17. package/frontend/js/store.js +39 -3
  18. package/frontend/js/utils/dom.js +28 -0
  19. package/frontend/js/utils/tableDesigner.js +8 -2
  20. package/frontend/js/views/charts.js +23 -43
  21. package/frontend/js/views/data.js +116 -132
  22. package/frontend/js/views/mediaTagging.js +131 -164
  23. package/frontend/js/views/structure.js +52 -48
  24. package/frontend/styles/tailwind.css +80 -0
  25. package/frontend/styles/tailwind.generated.css +2 -0
  26. package/frontend/styles/tokens.css +3 -3
  27. package/package.json +19 -5
  28. package/server/routes/mediaTagging.js +2 -10
  29. package/server/routes/sql.js +35 -10
  30. package/server/server.js +24 -0
  31. package/server/services/sqlite/dataBrowserService.js +25 -5
  32. package/server/services/sqlite/exportService.js +4 -2
  33. package/server/services/sqlite/introspection.js +2 -2
  34. package/server/services/sqlite/mediaTaggingService.js +166 -53
  35. package/server/services/sqlite/structureService.js +2 -2
  36. package/server/services/sqlite/tableDesigner/sql.js +19 -3
  37. package/server/services/storage/appStateStore.js +227 -87
  38. package/server/utils/appPaths.js +55 -19
  39. package/server/utils/fileValidation.js +94 -8
  40. package/tailwind.config.cjs +73 -0
  41. package/tests/security-paths.test.js +84 -0
  42. package/tests/sql-identifier-safety.test.js +66 -0
  43. package/.npmingnore +0 -4
  44. package/changelog.md +0 -84
  45. package/docs/DESIGN_GUIDELINES.md +0 -36
  46. package/scripts/publish_brew.sh +0 -466
  47. package/scripts/publish_npm.sh +0 -241
  48. package/shortkeys.md +0 -5
@@ -126,169 +126,98 @@ export function renderQueryHistoryDetail({
126
126
  `;
127
127
  }
128
128
 
129
- return `
130
- <section class="flex h-full min-h-0 flex-col bg-surface-low">
131
- <div class="border-b border-outline-variant/10 px-5 py-4">
132
- <div class="flex items-center justify-between gap-3">
133
- <div>
134
- <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/70">
135
- Query Detail
136
- </div>
137
- <h2 class="mt-1 font-headline text-lg font-black uppercase tracking-tight text-on-surface">
138
- ${escapeHtml(item.displayTitle)}
139
- </h2>
140
- </div>
141
- <button
142
- class="query-history-icon-button"
143
- data-action="clear-query-history-selection"
144
- type="button"
145
- >
146
- <span class="material-symbols-outlined text-[18px]">close</span>
147
- </button>
148
- </div>
149
- <div class="mt-4 flex flex-wrap gap-2">
150
- ${renderStatusBadge(item.queryType, getQueryTypeTone(item.queryType))}
151
- ${item.isSaved ? renderStatusBadge("saved", "primary") : ""}
152
- ${item.isDestructive ? renderStatusBadge("destructive", "warning") : ""}
153
- ${item.lastRun ? renderStatusBadge(item.lastRun.status, item.lastRun.status === "error" ? "alert" : "success") : ""}
154
- </div>
155
- </div>
156
- <div class="custom-scrollbar min-h-0 flex-1 overflow-auto px-5 py-5">
157
- <div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
158
- ${renderDetailMetaItem("Last Used", formatDateTime(item.lastUsedAt))}
159
- ${renderDetailMetaItem("First Executed", formatDateTime(item.firstExecutedAt))}
160
- ${renderDetailMetaItem("Use Count", formatNumber(item.useCount))}
161
- ${renderDetailMetaItem(
162
- "Tables",
163
- item.tablesDetected?.length ? item.tablesDetected.join(", ") : "None detected"
164
- )}
165
- </div>
129
+ const chartButton = canOpenQueryHistoryInCharts(item)
130
+ ? [
131
+ '<button class="standard-button" data-action="navigate" data-to="/charts/',
132
+ encodeURIComponent(item.id),
133
+ '" type="button"><span class="material-symbols-outlined text-sm">bar_chart</span>Open In Charts</button>',
134
+ ].join("")
135
+ : "";
136
+ const statusMarkup = [
137
+ renderStatusBadge(item.queryType, getQueryTypeTone(item.queryType)),
138
+ item.isSaved ? renderStatusBadge("saved", "primary") : "",
139
+ item.isDestructive ? renderStatusBadge("destructive", "warning") : "",
140
+ item.lastRun
141
+ ? renderStatusBadge(item.lastRun.status, item.lastRun.status === "error" ? "alert" : "success")
142
+ : "",
143
+ ].join("");
144
+ const metaMarkup = [
145
+ renderDetailMetaItem("Last Used", formatDateTime(item.lastUsedAt)),
146
+ renderDetailMetaItem("First Executed", formatDateTime(item.firstExecutedAt)),
147
+ renderDetailMetaItem("Use Count", formatNumber(item.useCount)),
148
+ renderDetailMetaItem(
149
+ "Tables",
150
+ item.tablesDetected?.length ? item.tablesDetected.join(", ") : "None detected"
151
+ ),
152
+ ].join("");
153
+ const runsMarkup = runs.length
154
+ ? runs.map((run) => renderRunItem(run)).join("")
155
+ : '<div class="border border-outline-variant/10 bg-surface-container px-3 py-4 text-sm text-on-surface-variant/65">No execution runs recorded yet.</div>';
166
156
 
167
- <form class="mt-5" data-form="save-query-history-title">
168
- <input name="historyId" type="hidden" value="${escapeHtml(item.id)}" />
169
- <label class="block text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
170
- Custom Title
171
- </label>
172
- <div class="mt-2 flex gap-2">
173
- <input
174
- class="control-input flex-1 border border-outline-variant/20 bg-surface-container text-sm text-on-surface outline-none placeholder:text-on-surface-variant/35 focus:border-primary-container"
175
- name="title"
176
- placeholder="${escapeHtml(item.displayTitle)}"
177
- type="text"
178
- value="${escapeHtml(item.title ?? "")}"
179
- />
180
- <button
181
- class="standard-button"
182
- type="submit"
183
- >
184
- Save
185
- </button>
186
- </div>
187
- <p class="mt-2 text-xs text-on-surface-variant/60">
188
- Leave empty to fall back to the auto title generated from the query.
189
- </p>
190
- </form>
191
-
192
- <div class="mt-5 flex flex-wrap gap-2">
193
- <button
194
- class="standard-button"
195
- data-action="open-query-history"
196
- data-history-id="${escapeHtml(item.id)}"
197
- type="button"
198
- >
199
- Open In Editor
200
- </button>
201
- ${
202
- canOpenQueryHistoryInCharts(item)
203
- ? `
204
- <button
205
- class="standard-button"
206
- data-action="navigate"
207
- data-to="/charts/${encodeURIComponent(item.id)}"
208
- type="button"
209
- >
210
- <span class="material-symbols-outlined text-sm">bar_chart</span>
211
- Open In Charts
212
- </button>
213
- `
214
- : ""
215
- }
216
- <button
217
- class="standard-button"
218
- data-action="run-query-history"
219
- data-history-id="${escapeHtml(item.id)}"
220
- type="button"
221
- >
222
- Run Now
223
- </button>
224
- <button
225
- class="standard-button"
226
- data-action="toggle-query-history-saved"
227
- data-history-id="${escapeHtml(item.id)}"
228
- data-next-value="${item.isSaved ? "false" : "true"}"
229
- type="button"
230
- >
231
- ${item.isSaved ? "Unsave" : "Save"}
232
- </button>
233
- <button
234
- class="delete-button"
235
- data-action="open-delete-query-history-modal"
236
- data-history-id="${escapeHtml(item.id)}"
237
- type="button"
238
- >
239
- Delete
240
- </button>
241
- </div>
242
-
243
- <div class="mt-6">
244
- <div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
245
- SQL
246
- </div>
247
- <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(
248
- item.rawSql
249
- )}</code></pre>
250
- </div>
251
-
252
- <form class="mt-6" data-form="save-query-history-notes">
253
- <input name="historyId" type="hidden" value="${escapeHtml(item.id)}" />
254
- <label class="block text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
255
- Notes
256
- </label>
257
- <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(
258
- item.notes ?? ""
259
- )}</textarea>
260
- <div class="mt-2 flex justify-end">
261
- <button
262
- class="standard-button"
263
- type="submit"
264
- >
265
- Save Notes
266
- </button>
267
- </div>
268
- </form>
269
-
270
- <div class="mt-6">
271
- <div class="flex items-center justify-between gap-3">
272
- <div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
273
- Latest Runs
274
- </div>
275
- <div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/40">
276
- ${escapeHtml(formatNumber(runs.length))}
277
- </div>
278
- </div>
279
- <div class="mt-3 space-y-3">
280
- ${
281
- runs.length
282
- ? runs.map((run) => renderRunItem(run)).join("")
283
- : `
284
- <div class="border border-outline-variant/10 bg-surface-container px-3 py-4 text-sm text-on-surface-variant/65">
285
- No execution runs recorded yet.
286
- </div>
287
- `
288
- }
289
- </div>
290
- </div>
291
- </div>
292
- </section>
293
- `;
157
+ return [
158
+ '<section class="flex h-full min-h-0 flex-col bg-surface-low">',
159
+ '<div class="border-b border-outline-variant/10 px-5 py-4">',
160
+ '<div class="flex items-center justify-between gap-3"><div>',
161
+ '<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/70">Query Detail</div>',
162
+ '<h2 class="mt-1 font-headline text-lg font-black uppercase tracking-tight text-on-surface">',
163
+ escapeHtml(item.displayTitle),
164
+ "</h2></div>",
165
+ '<button class="query-history-icon-button" data-action="clear-query-history-selection" type="button"><span class="material-symbols-outlined text-[18px]">close</span></button>',
166
+ '</div><div class="mt-4 flex flex-wrap gap-2">',
167
+ statusMarkup,
168
+ "</div></div>",
169
+ '<div class="custom-scrollbar min-h-0 flex-1 overflow-auto px-5 py-5">',
170
+ '<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">',
171
+ metaMarkup,
172
+ "</div>",
173
+ '<form class="mt-5" data-form="save-query-history-title">',
174
+ '<input name="historyId" type="hidden" value="',
175
+ escapeHtml(item.id),
176
+ '" />',
177
+ '<label class="block text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">Custom Title</label>',
178
+ '<div class="mt-2 flex gap-2"><input class="control-input flex-1 border border-outline-variant/20 bg-surface-container text-sm text-on-surface outline-none placeholder:text-on-surface-variant/35 focus:border-primary-container" name="title" placeholder="',
179
+ escapeHtml(item.displayTitle),
180
+ '" type="text" value="',
181
+ escapeHtml(item.title ?? ""),
182
+ '" /><button class="standard-button" type="submit">Save</button></div>',
183
+ '<p class="mt-2 text-xs text-on-surface-variant/60">Leave empty to fall back to the auto title generated from the query.</p>',
184
+ "</form>",
185
+ '<div class="mt-5 flex flex-wrap gap-2">',
186
+ '<button class="standard-button" data-action="open-query-history" data-history-id="',
187
+ escapeHtml(item.id),
188
+ '" type="button">Open In Editor</button>',
189
+ chartButton,
190
+ '<button class="standard-button" data-action="run-query-history" data-history-id="',
191
+ escapeHtml(item.id),
192
+ '" type="button">Run Now</button>',
193
+ '<button class="standard-button" data-action="toggle-query-history-saved" data-history-id="',
194
+ escapeHtml(item.id),
195
+ '" data-next-value="',
196
+ item.isSaved ? "false" : "true",
197
+ '" type="button">',
198
+ item.isSaved ? "Unsave" : "Save",
199
+ "</button>",
200
+ '<button class="delete-button" data-action="open-delete-query-history-modal" data-history-id="',
201
+ escapeHtml(item.id),
202
+ '" type="button">Delete</button></div>',
203
+ '<div class="mt-6"><div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">SQL</div>',
204
+ '<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>',
205
+ highlightSql(item.rawSql),
206
+ "</code></pre></div>",
207
+ '<form class="mt-6" data-form="save-query-history-notes">',
208
+ '<input name="historyId" type="hidden" value="',
209
+ escapeHtml(item.id),
210
+ '" />',
211
+ '<label class="block text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">Notes</label>',
212
+ '<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...">',
213
+ escapeHtml(item.notes ?? ""),
214
+ '</textarea><div class="mt-2 flex justify-end"><button class="standard-button" type="submit">Save Notes</button></div></form>',
215
+ '<div class="mt-6"><div class="flex items-center justify-between gap-3">',
216
+ '<div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">Latest Runs</div>',
217
+ '<div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/40">',
218
+ escapeHtml(formatNumber(runs.length)),
219
+ '</div></div><div class="mt-3 space-y-3">',
220
+ runsMarkup,
221
+ "</div></div></div></section>",
222
+ ].join("");
294
223
  }
@@ -20,85 +20,69 @@ export function renderQueryHistoryListItem(item, activeHistoryId, selectedHistor
20
20
  const isActive = Number(activeHistoryId) === Number(item.id);
21
21
  const isSelected = Number(selectedHistoryId) === Number(item.id);
22
22
  const visibleTables = (item.tablesDetected ?? []).slice(0, 3);
23
+ const itemClasses = [
24
+ "query-history-item",
25
+ isActive ? "is-active" : "",
26
+ item.lastRun?.status === "error" ? "is-error" : "",
27
+ ]
28
+ .filter(Boolean)
29
+ .join(" ");
30
+ const tableMarkup = visibleTables
31
+ .map((tableName) =>
32
+ [
33
+ '<span class="border border-outline-variant/20 bg-surface-highest px-2 py-1">',
34
+ escapeHtml(tableName),
35
+ "</span>",
36
+ ].join("")
37
+ )
38
+ .join("");
23
39
 
24
- return `
25
- <article class="query-history-item ${isActive ? "is-active" : ""} ${
26
- item.lastRun?.status === "error" ? "is-error" : ""
27
- }">
28
- <button
29
- class="query-history-item-hit ${isActive ? "is-active" : ""}"
30
- data-action="select-query-history-item"
31
- data-history-id="${escapeHtml(item.id)}"
32
- type="button"
33
- >
34
- <div class="flex flex-wrap items-center gap-2">
35
- <span class="truncate font-headline text-sm font-bold uppercase tracking-tight text-on-surface">
36
- ${escapeHtml(item.displayTitle)}
37
- </span>
38
- ${renderStatusBadge(item.queryType, getQueryTypeTone(item.queryType))}
39
- ${item.isSaved ? renderStatusBadge("saved", "primary") : ""}
40
- ${item.isDestructive ? renderStatusBadge("destructive", "warning") : ""}
41
- </div>
42
- <p class="query-history-sql-preview mt-2 text-left font-mono text-xs leading-5 text-on-surface-variant/75">
43
- ${escapeHtml(item.previewSql)}
44
- </p>
45
- </button>
46
- <div class="flex items-center justify-between gap-3 border-t border-outline-variant/10 px-3 pb-3 pt-2">
47
- <div class="min-w-0 flex flex-wrap gap-2 text-[10px] font-mono uppercase tracking-[0.14em] text-on-surface-variant/55">
48
- ${visibleTables
49
- .map(
50
- (tableName) => `
51
- <span class="border border-outline-variant/20 bg-surface-highest px-2 py-1">
52
- ${escapeHtml(tableName)}
53
- </span>
54
- `
55
- )
56
- .join("")}
57
- </div>
58
- <div class="flex items-center gap-1">
59
- <button
60
- class="query-history-icon-button"
61
- data-action="open-query-history"
62
- data-history-id="${escapeHtml(item.id)}"
63
- title="Open in editor"
64
- type="button"
65
- >
66
- <span class="material-symbols-outlined text-[18px]">edit_note</span>
67
- </button>
68
- <button
69
- class="query-history-icon-button"
70
- data-action="run-query-history"
71
- data-history-id="${escapeHtml(item.id)}"
72
- title="Run query"
73
- type="button"
74
- >
75
- <span class="material-symbols-outlined text-[18px]">play_arrow</span>
76
- </button>
77
- <button
78
- class="query-history-icon-button ${item.isSaved ? "is-active" : ""}"
79
- data-action="toggle-query-history-saved"
80
- data-history-id="${escapeHtml(item.id)}"
81
- data-next-value="${item.isSaved ? "false" : "true"}"
82
- title="${item.isSaved ? "Remove from saved" : "Save query"}"
83
- type="button"
84
- >
85
- <span class="material-symbols-outlined text-[18px]">
86
- ${item.isSaved ? "bookmark" : "bookmark_add"}
87
- </span>
88
- </button>
89
- <button
90
- class="query-history-icon-button ${isSelected ? "is-active" : ""}"
91
- data-action="select-query-history-item"
92
- data-history-id="${escapeHtml(item.id)}"
93
- title="Open query detail"
94
- type="button"
95
- >
96
- <span class="material-symbols-outlined text-[18px]">info</span>
97
- </button>
98
- </div>
99
- </div>
100
- </article>
101
- `;
40
+ return [
41
+ '<article class="',
42
+ itemClasses,
43
+ '"><button class="query-history-item-hit ',
44
+ isActive ? "is-active" : "",
45
+ '" data-action="select-query-history-item" data-history-id="',
46
+ escapeHtml(item.id),
47
+ '" type="button">',
48
+ '<div class="flex flex-wrap items-center gap-2"><span class="truncate font-headline text-sm font-bold uppercase tracking-tight text-on-surface">',
49
+ escapeHtml(item.displayTitle),
50
+ "</span>",
51
+ renderStatusBadge(item.queryType, getQueryTypeTone(item.queryType)),
52
+ item.isSaved ? renderStatusBadge("saved", "primary") : "",
53
+ item.isDestructive ? renderStatusBadge("destructive", "warning") : "",
54
+ "</div>",
55
+ '<p class="query-history-sql-preview mt-2 text-left font-mono text-xs leading-5 text-on-surface-variant/75">',
56
+ escapeHtml(item.previewSql),
57
+ "</p></button>",
58
+ '<div class="flex items-center justify-between gap-3 border-t border-outline-variant/10 px-3 pb-3 pt-2">',
59
+ '<div class="min-w-0 flex flex-wrap gap-2 text-[10px] font-mono uppercase tracking-[0.14em] text-on-surface-variant/55">',
60
+ tableMarkup,
61
+ '</div><div class="flex items-center gap-1">',
62
+ '<button class="query-history-icon-button" data-action="open-query-history" data-history-id="',
63
+ escapeHtml(item.id),
64
+ '" title="Open in editor" type="button"><span class="material-symbols-outlined text-[18px]">edit_note</span></button>',
65
+ '<button class="query-history-icon-button" data-action="run-query-history" data-history-id="',
66
+ escapeHtml(item.id),
67
+ '" title="Run query" type="button"><span class="material-symbols-outlined text-[18px]">play_arrow</span></button>',
68
+ '<button class="query-history-icon-button ',
69
+ item.isSaved ? "is-active" : "",
70
+ '" data-action="toggle-query-history-saved" data-history-id="',
71
+ escapeHtml(item.id),
72
+ '" data-next-value="',
73
+ item.isSaved ? "false" : "true",
74
+ '" title="',
75
+ item.isSaved ? "Remove from saved" : "Save query",
76
+ '" type="button"><span class="material-symbols-outlined text-[18px]">',
77
+ item.isSaved ? "bookmark" : "bookmark_add",
78
+ "</span></button>",
79
+ '<button class="query-history-icon-button ',
80
+ isSelected ? "is-active" : "",
81
+ '" data-action="select-query-history-item" data-history-id="',
82
+ escapeHtml(item.id),
83
+ '" title="Open query detail" type="button"><span class="material-symbols-outlined text-[18px]">info</span></button>',
84
+ "</div></div></article>",
85
+ ].join("");
102
86
  }
103
87
 
104
88
  function renderQueryHistoryTabs(activeTab, historyTotal) {
@@ -109,27 +93,25 @@ function renderQueryHistoryTabs(activeTab, historyTotal) {
109
93
  { id: "failed", label: "Failed" },
110
94
  ];
111
95
 
112
- return `
113
- <div class="flex items-center gap-2">
114
- ${tabs
115
- .map(
116
- (tab) => `
117
- <button
118
- class="query-history-tab ${activeTab === tab.id ? "is-active" : ""}"
119
- data-action="set-query-history-tab"
120
- data-tab="${tab.id}"
121
- type="button"
122
- >
123
- ${escapeHtml(tab.label)}
124
- </button>
125
- `
126
- )
127
- .join("")}
128
- <span class="ml-auto text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/50">
129
- ${escapeHtml(formatNumber(historyTotal))}
130
- </span>
131
- </div>
132
- `;
96
+ return [
97
+ '<div class="flex items-center gap-2">',
98
+ tabs
99
+ .map((tab) =>
100
+ [
101
+ '<button class="query-history-tab ',
102
+ activeTab === tab.id ? "is-active" : "",
103
+ '" data-action="set-query-history-tab" data-tab="',
104
+ tab.id,
105
+ '" type="button">',
106
+ escapeHtml(tab.label),
107
+ "</button>",
108
+ ].join("")
109
+ )
110
+ .join(""),
111
+ '<span class="ml-auto text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/50">',
112
+ escapeHtml(formatNumber(historyTotal)),
113
+ "</span></div>",
114
+ ].join("");
133
115
  }
134
116
 
135
117
  export function renderQueryHistoryPanel({
@@ -76,7 +76,9 @@ export function renderQueryResultsPane(
76
76
  editable ? "cursor-pointer hover:bg-surface-bright" : "hover:bg-surface-bright"
77
77
  }`,
78
78
  getRowAttrs: (_, index) =>
79
- editable ? `data-action="select-editor-row" data-row-index="${index}"` : "",
79
+ editable
80
+ ? ['data-action="select-editor-row" data-row-index="', index, '"'].join("")
81
+ : "",
80
82
  })
81
83
  : `
82
84
  <div class="flex h-full flex-col items-center justify-center text-center text-on-surface-variant/35">
@@ -149,43 +149,40 @@ export function renderRowEditorPanel({
149
149
  }) {
150
150
  const canSubmit = !disabledMessage && editableFields.length > 0;
151
151
  const canDelete = !disabledMessage && deleteEnabled;
152
- const formId = `${formName}-panel-form`;
152
+ const formId = [formName, "-panel-form"].join("");
153
153
  const headerActions = [
154
154
  reloadAction
155
- ? `
156
- <button
157
- class="standard-button"
158
- data-action="${escapeHtml(reloadAction)}"
159
- type="button"
160
- >
161
- Reload
162
- </button>
163
- `
155
+ ? [
156
+ '<button class="standard-button" data-action="',
157
+ escapeHtml(reloadAction),
158
+ '" type="button">Reload</button>',
159
+ ].join("")
164
160
  : "",
165
161
  canSubmit
166
- ? `
167
- <button
168
- class="standard-button"
169
- form="${escapeHtml(formId)}"
170
- type="submit"
171
- ${saving || deleting ? "disabled" : ""}
172
- >
173
- ${escapeHtml(saving ? "Saving..." : submitLabel)}
174
- </button>
175
- `
162
+ ? [
163
+ '<button class="standard-button" form="',
164
+ escapeHtml(formId),
165
+ '" type="submit" ',
166
+ saving || deleting ? "disabled" : "",
167
+ ">",
168
+ escapeHtml(saving ? "Saving..." : submitLabel),
169
+ "</button>",
170
+ ].join("")
176
171
  : "",
177
172
  canDelete
178
- ? `
179
- <button
180
- class="delete-button"
181
- data-action="${escapeHtml(deleteAction)}"
182
- ${deleteRowIndex === null ? "" : `data-row-index="${escapeHtml(String(deleteRowIndex))}"`}
183
- type="button"
184
- ${saving || deleting ? "disabled" : ""}
185
- >
186
- ${escapeHtml(deleting ? "Deleting..." : deleteLabel)}
187
- </button>
188
- `
173
+ ? [
174
+ '<button class="delete-button" data-action="',
175
+ escapeHtml(deleteAction),
176
+ '" ',
177
+ deleteRowIndex === null
178
+ ? ""
179
+ : ['data-row-index="', escapeHtml(String(deleteRowIndex)), '"'].join(""),
180
+ ' type="button" ',
181
+ saving || deleting ? "disabled" : "",
182
+ ">",
183
+ escapeHtml(deleting ? "Deleting..." : deleteLabel),
184
+ "</button>",
185
+ ].join("")
189
186
  : "",
190
187
  ]
191
188
  .filter(Boolean)
@@ -1,4 +1,5 @@
1
1
  import { showToast } from '../store.js';
2
+ import { replaceChildrenFromRenderedMarkup } from '../utils/dom.js';
2
3
  import { escapeHtml, formatNumber } from '../utils/format.js';
3
4
 
4
5
  let cytoscapeFactory = null;
@@ -530,12 +531,12 @@ function updateInspectorToggleButton() {
530
531
  }
531
532
 
532
533
  currentGraph.inspectorToggleButton.classList.toggle('is-active', currentGraph.inspectorHidden);
533
- currentGraph.inspectorToggleButton.innerHTML = `
534
- <span class="material-symbols-outlined text-sm">${
535
- currentGraph.inspectorHidden ? 'right_panel_open' : 'right_panel_close'
536
- }</span>
537
- ${currentGraph.inspectorHidden ? 'Show Inspector' : 'Hide Inspector'}
538
- `;
534
+ const icon = document.createElement('span');
535
+ const label = currentGraph.inspectorHidden ? 'Show Inspector' : 'Hide Inspector';
536
+
537
+ icon.className = 'material-symbols-outlined text-sm';
538
+ icon.textContent = currentGraph.inspectorHidden ? 'right_panel_open' : 'right_panel_close';
539
+ currentGraph.inspectorToggleButton.replaceChildren(icon, document.createTextNode(` ${label}`));
539
540
  }
540
541
 
541
542
  function clearSelection() {
@@ -546,7 +547,7 @@ function clearSelection() {
546
547
  currentGraph.selectedTableName = null;
547
548
  resetHighlights(currentGraph.cy);
548
549
  syncEntryHighlights();
549
- currentGraph.inspector.innerHTML = getDefaultInspectorMarkup();
550
+ replaceChildrenFromRenderedMarkup(currentGraph.inspector, getDefaultInspectorMarkup());
550
551
  updateOpenDataButton();
551
552
  }
552
553
 
@@ -557,7 +558,7 @@ function applyTableSelection(node, { focus = true } = {}) {
557
558
 
558
559
  const tableData = node.data('table');
559
560
  currentGraph.selectedTableName = tableData.name;
560
- currentGraph.inspector.innerHTML = renderInspector(tableData);
561
+ replaceChildrenFromRenderedMarkup(currentGraph.inspector, renderInspector(tableData));
561
562
  highlightConnectedElements(currentGraph.cy, node);
562
563
  syncEntryHighlights([tableData.name]);
563
564
  updateOpenDataButton();
@@ -742,7 +743,7 @@ export async function mountStructureGraph(snapshot) {
742
743
 
743
744
  if (!schema.tables?.length) {
744
745
  empty?.removeAttribute('hidden');
745
- inspector.innerHTML = clearInspector();
746
+ replaceChildrenFromRenderedMarkup(inspector, clearInspector());
746
747
  return;
747
748
  }
748
749