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.
Files changed (75) hide show
  1. package/README.md +5 -5
  2. package/changelog.md +14 -0
  3. package/{js → frontend/js}/api.js +99 -5
  4. package/{js → frontend/js}/app.js +162 -11
  5. package/{js → frontend/js}/components/connectionCard.js +8 -9
  6. package/frontend/js/components/connectionLogo.js +33 -0
  7. package/{js → frontend/js}/components/dataGrid.js +3 -3
  8. package/{js → frontend/js}/components/emptyState.js +25 -11
  9. package/{js → frontend/js}/components/modal.js +57 -0
  10. package/{js → frontend/js}/components/queryEditor.js +33 -55
  11. package/frontend/js/components/queryHistoryDetail.js +263 -0
  12. package/frontend/js/components/queryHistoryPanel.js +228 -0
  13. package/{js → frontend/js}/components/queryResults.js +32 -46
  14. package/{js → frontend/js}/components/rowEditorPanel.js +73 -14
  15. package/{js → frontend/js}/components/sidebar.js +8 -3
  16. package/{js → frontend/js}/store.js +577 -21
  17. package/{js → frontend/js}/utils/format.js +23 -0
  18. package/{js → frontend/js}/views/data.js +136 -10
  19. package/{js → frontend/js}/views/editor.js +34 -10
  20. package/{js → frontend/js}/views/overview.js +15 -0
  21. package/{js → frontend/js}/views/structure.js +10 -12
  22. package/{styles → frontend/styles}/components.css +106 -0
  23. package/{styles → frontend/styles}/structure-graph.css +5 -10
  24. package/package.json +2 -2
  25. package/{publish_brew.sh → scripts/publish_brew.sh} +2 -2
  26. package/{publish_npm.sh → scripts/publish_npm.sh} +2 -2
  27. package/server/data/db_logos/.gitkeep +0 -0
  28. package/server/routes/connections.js +2 -0
  29. package/server/routes/data.js +2 -0
  30. package/server/routes/export.js +4 -1
  31. package/server/routes/overview.js +12 -0
  32. package/server/routes/sql.js +163 -5
  33. package/server/server.js +8 -6
  34. package/server/services/sqlite/connectionManager.js +68 -33
  35. package/server/services/sqlite/dataBrowserService.js +4 -16
  36. package/server/services/sqlite/exportService.js +4 -16
  37. package/server/services/sqlite/overviewService.js +34 -0
  38. package/server/services/sqlite/sqlExecutor.js +83 -63
  39. package/server/services/sqlite/tableSort.js +63 -0
  40. package/server/services/storage/appStateStore.js +832 -20
  41. package/server/services/storage/queryHistoryUtils.js +169 -0
  42. package/server/utils/appPaths.js +42 -18
  43. package/{assets → frontend/assets}/images/logo.webp +0 -0
  44. package/{assets → frontend/assets}/images/logo_extrasmall.webp +0 -0
  45. package/{assets → frontend/assets}/images/logo_raw.png +0 -0
  46. package/{assets → frontend/assets}/images/logo_small.webp +0 -0
  47. package/{assets → frontend/assets}/mockups/connections.png +0 -0
  48. package/{assets → frontend/assets}/mockups/data.png +0 -0
  49. package/{assets → frontend/assets}/mockups/data_edit.png +0 -0
  50. package/{assets → frontend/assets}/mockups/graph_visualize.png +0 -0
  51. package/{assets → frontend/assets}/mockups/home.png +0 -0
  52. package/{assets → frontend/assets}/mockups/overview.png +0 -0
  53. package/{assets → frontend/assets}/mockups/sql_editor.png +0 -0
  54. package/{assets → frontend/assets}/mockups/structure.png +0 -0
  55. package/{index.html → frontend/index.html} +0 -0
  56. package/{js → frontend/js}/components/actionBar.js +0 -0
  57. package/{js → frontend/js}/components/appShell.js +0 -0
  58. package/{js → frontend/js}/components/badges.js +0 -0
  59. package/{js → frontend/js}/components/bottomTabs.js +1 -1
  60. /package/{js → frontend/js}/components/metricCard.js +0 -0
  61. /package/{js → frontend/js}/components/pageHeader.js +0 -0
  62. /package/{js → frontend/js}/components/statusBar.js +0 -0
  63. /package/{js → frontend/js}/components/structureGraph.js +0 -0
  64. /package/{js → frontend/js}/components/toast.js +0 -0
  65. /package/{js → frontend/js}/components/topNav.js +0 -0
  66. /package/{js → frontend/js}/lib/cytoscapeRuntime.js +0 -0
  67. /package/{js → frontend/js}/router.js +0 -0
  68. /package/{js → frontend/js}/views/connections.js +0 -0
  69. /package/{js → frontend/js}/views/landing.js +0 -0
  70. /package/{js → frontend/js}/views/settings.js +0 -0
  71. /package/{styles → frontend/styles}/base.css +0 -0
  72. /package/{styles → frontend/styles}/layout.css +0 -0
  73. /package/{styles → frontend/styles}/tokens.css +0 -0
  74. /package/{styles → frontend/styles}/views.css +0 -0
  75. /package/{data → server/data}/.gitkeep +0 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # sqlite-hub ⚡️
2
2
 
3
- ![](/assets/mockups/home.png)
3
+ ![](./frontend/assets/mockups/home.png)
4
4
 
5
5
  A focused local-first app for browsing, editing, and querying SQLite databases.
6
6
 
@@ -24,25 +24,25 @@ SQLite Hub keeps that workflow sharp:
24
24
 
25
25
  ### Structure view
26
26
 
27
- ![](/assets/mockups/graph_visualize.png)
27
+ ![](./frontend/assets/mockups/graph_visualize.png)
28
28
 
29
29
  Inspect tables, columns, types, and schema details without losing pace. Visualized in a graph.
30
30
 
31
31
  ### Data browser
32
32
 
33
- ![](/assets/mockups/data.png)
33
+ ![](./frontend/assets/mockups/data.png)
34
34
 
35
35
  Scan rows, sort fast, move through local data quickly, and export full tables as CSV.
36
36
 
37
37
  ### Row editing
38
38
 
39
- ![](/assets/mockups/data_edit.png)
39
+ ![](./frontend/assets/mockups/data_edit.png)
40
40
 
41
41
  Open one record, edit it in place, commit, continue.
42
42
 
43
43
  ### SQL editor
44
44
 
45
- ![](/assets/mockups/sql_editor.png)
45
+ ![](./frontend/assets/mockups/sql_editor.png)
46
46
 
47
47
  Write queries in a syntax-highlighted editor, inspect results in the same workflow, and export result sets as CSV.
48
48
 
package/changelog.md CHANGED
@@ -1,3 +1,17 @@
1
+ # v0.5.0
2
+
3
+ - sql query history
4
+ - open in finder in overview
5
+ - sort columns in data & sql_editor
6
+ - json view in row editor
7
+
8
+ # v0.4.0
9
+
10
+ - search in data
11
+ - shift + enter
12
+ - logos for databases
13
+ - clean up some chaotic code things :)
14
+
1
15
  # v0.3.2
2
16
 
3
17
  - delete rows
@@ -123,6 +123,12 @@ export function getOverview() {
123
123
  return request("/api/db/overview");
124
124
  }
125
125
 
126
+ export function openOverviewInFinder() {
127
+ return request("/api/db/overview/open-in-finder", {
128
+ method: "POST",
129
+ });
130
+ }
131
+
126
132
  export function getDbStatus() {
127
133
  return request("/api/db/status");
128
134
  }
@@ -134,16 +140,92 @@ export function executeSql(sql) {
134
140
  });
135
141
  }
136
142
 
137
- export function getSqlHistory() {
138
- return request("/api/sql/history");
143
+ export function getQueryHistory(options = {}) {
144
+ const params = new URLSearchParams();
145
+
146
+ if (options.tab) {
147
+ params.set("tab", String(options.tab));
148
+ }
149
+
150
+ if (options.limit !== undefined) {
151
+ params.set("limit", String(options.limit));
152
+ }
153
+
154
+ if (options.offset !== undefined) {
155
+ params.set("offset", String(options.offset));
156
+ }
157
+
158
+ if (options.search) {
159
+ params.set("search", String(options.search));
160
+ }
161
+
162
+ if (options.queryType) {
163
+ params.set("queryType", String(options.queryType));
164
+ }
165
+
166
+ if (options.onlySaved) {
167
+ params.set("onlySaved", "true");
168
+ }
169
+
170
+ if (options.onlyFavorites) {
171
+ params.set("onlyFavorites", "true");
172
+ }
173
+
174
+ const query = params.toString();
175
+ return request(`/api/sql/history${query ? `?${query}` : ""}`);
139
176
  }
140
177
 
141
- export function clearSqlHistory() {
178
+ export function clearQueryHistory() {
142
179
  return request("/api/sql/history", {
143
180
  method: "DELETE",
144
181
  });
145
182
  }
146
183
 
184
+ export function getQueryHistoryItem(historyId) {
185
+ return request(`/api/sql/history/${encodeURIComponent(historyId)}`);
186
+ }
187
+
188
+ export function getQueryHistoryRuns(historyId, options = {}) {
189
+ const params = new URLSearchParams();
190
+
191
+ if (options.limit !== undefined) {
192
+ params.set("limit", String(options.limit));
193
+ }
194
+
195
+ const query = params.toString();
196
+
197
+ return request(
198
+ `/api/sql/history/${encodeURIComponent(historyId)}/runs${query ? `?${query}` : ""}`
199
+ );
200
+ }
201
+
202
+ export function toggleQueryHistorySaved(historyId, value) {
203
+ return request(`/api/sql/history/${encodeURIComponent(historyId)}/saved`, {
204
+ method: "PATCH",
205
+ body: { value },
206
+ });
207
+ }
208
+
209
+ export function renameQueryHistoryItem(historyId, title) {
210
+ return request(`/api/sql/history/${encodeURIComponent(historyId)}/title`, {
211
+ method: "PATCH",
212
+ body: { title },
213
+ });
214
+ }
215
+
216
+ export function updateQueryHistoryNotes(historyId, notes) {
217
+ return request(`/api/sql/history/${encodeURIComponent(historyId)}/notes`, {
218
+ method: "PATCH",
219
+ body: { notes },
220
+ });
221
+ }
222
+
223
+ export function deleteQueryHistoryItem(historyId) {
224
+ return request(`/api/sql/history/${encodeURIComponent(historyId)}`, {
225
+ method: "DELETE",
226
+ });
227
+ }
228
+
147
229
  export function getStructureOverview() {
148
230
  return request("/api/structure");
149
231
  }
@@ -167,6 +249,14 @@ export function getDataTable(tableName, options = {}) {
167
249
  params.set("offset", String(options.offset));
168
250
  }
169
251
 
252
+ if (options.sortColumn) {
253
+ params.set("sortColumn", String(options.sortColumn));
254
+ }
255
+
256
+ if (options.sortDirection) {
257
+ params.set("sortDirection", String(options.sortDirection));
258
+ }
259
+
170
260
  const query = params.toString();
171
261
 
172
262
  return request(`/api/data/${encodeURIComponent(tableName)}${query ? `?${query}` : ""}`);
@@ -205,10 +295,14 @@ export function downloadQueryCsv(sql) {
205
295
  });
206
296
  }
207
297
 
208
- export function downloadTableCsv(tableName) {
298
+ export function downloadTableCsv(tableName, options = {}) {
209
299
  return download("/api/export/table.csv", {
210
300
  method: "POST",
211
- body: { tableName },
301
+ body: {
302
+ tableName,
303
+ sortColumn: options.sortColumn,
304
+ sortDirection: options.sortDirection,
305
+ },
212
306
  fallbackFilename: `${tableName || "table"}.csv`,
213
307
  });
214
308
  }
@@ -16,30 +16,43 @@ import {
16
16
  clearDataRowSelection,
17
17
  clearEditorRowSelection,
18
18
  clearEditorResults,
19
- clearSqlHistoryStateAndData,
19
+ clearQueryHistorySelection,
20
20
  closeModal,
21
+ deleteQueryHistoryStateItem,
21
22
  dismissToast,
22
23
  executeCurrentQuery,
23
24
  exportCurrentDataTableCsv,
24
25
  exportCurrentQueryCsv,
25
26
  getState,
26
27
  initializeApp,
27
- loadQueryFromHistory,
28
+ loadMoreQueryHistory,
28
29
  openModal,
30
+ openOverviewInFinder,
31
+ openQueryHistoryInEditor,
29
32
  openDeleteDataRowModal,
30
33
  openDeleteEditorRowModal,
31
34
  openEditConnectionModal,
32
35
  refreshCurrentRoute,
33
36
  removeConnection,
37
+ runQueryHistoryItem,
34
38
  selectDataRow,
35
39
  selectEditorRow,
36
40
  selectConnection,
41
+ selectQueryHistoryItem,
37
42
  selectStructureEntry,
38
43
  setDataPage,
39
44
  setDataPageSize,
45
+ setDataSearchColumn,
46
+ setDataSearchQuery,
40
47
  setCurrentQuery,
41
48
  setEditorTab,
49
+ sortDataTableByColumn,
50
+ sortEditorResultsByColumn,
51
+ setQueryHistorySearchInput,
52
+ setQueryHistoryTab,
42
53
  setRoute,
54
+ saveQueryHistoryNotes,
55
+ saveQueryHistoryTitle,
43
56
  submitCreateConnection,
44
57
  submitDeleteRowConfirmation,
45
58
  submitDataRowUpdate,
@@ -48,6 +61,7 @@ import {
48
61
  submitImportSql,
49
62
  submitOpenConnection,
50
63
  subscribe,
64
+ toggleQueryHistorySavedState,
51
65
  } from "./store.js";
52
66
  import { renderConnectionsView } from "./views/connections.js";
53
67
  import { renderDataView } from "./views/data.js";
@@ -115,6 +129,35 @@ function syncQueryEditorScroll(textarea) {
115
129
  highlightNode.style.transform = `translate(${-textarea.scrollLeft}px, ${-textarea.scrollTop}px)`;
116
130
  }
117
131
 
132
+ function readFileAsBase64(file) {
133
+ return new Promise((resolve, reject) => {
134
+ const reader = new FileReader();
135
+
136
+ reader.onload = () => {
137
+ const result = String(reader.result ?? "");
138
+ const markerIndex = result.indexOf(",");
139
+ resolve(markerIndex >= 0 ? result.slice(markerIndex + 1) : result);
140
+ };
141
+ reader.onerror = () => {
142
+ reject(reader.error ?? new Error("The selected logo could not be read."));
143
+ };
144
+
145
+ reader.readAsDataURL(file);
146
+ });
147
+ }
148
+
149
+ async function buildConnectionLogoUpload(file) {
150
+ if (!(file instanceof File) || !file.size) {
151
+ return null;
152
+ }
153
+
154
+ return {
155
+ fileName: file.name,
156
+ mimeType: file.type,
157
+ base64: await readFileAsBase64(file),
158
+ };
159
+ }
160
+
118
161
  function renderNotFoundView() {
119
162
  return {
120
163
  main: `
@@ -224,7 +267,9 @@ function restoreFocusedInputState(snapshot) {
224
267
  function renderApp(state) {
225
268
  const focusedInput = captureFocusedInputState();
226
269
  const { main, panel } = resolveView(state);
227
- const isLockedRoute = ["editor", "editorResults", "data"].includes(state.route.name);
270
+ const isLockedRoute = ["editor", "editorResults", "data", "structure"].includes(
271
+ state.route.name
272
+ );
228
273
 
229
274
  teardownStructureGraph();
230
275
  shellRefs.topNav.innerHTML = renderTopNav(state);
@@ -249,6 +294,11 @@ const router = createRouter((route) => {
249
294
  setRoute(route);
250
295
  });
251
296
 
297
+ async function executeEditorQueryAndNavigate() {
298
+ const success = await executeCurrentQuery();
299
+ router.navigate(success ? "/editor/results" : "/editor");
300
+ }
301
+
252
302
  async function handleAction(actionNode) {
253
303
  const { action } = actionNode.dataset;
254
304
 
@@ -300,9 +350,11 @@ async function handleAction(actionNode) {
300
350
  case "create-backup":
301
351
  await createActiveConnectionBackup();
302
352
  return;
353
+ case "open-overview-in-finder":
354
+ await openOverviewInFinder();
355
+ return;
303
356
  case "execute-query": {
304
- const success = await executeCurrentQuery();
305
- router.navigate(success ? "/editor/results" : "/editor");
357
+ await executeEditorQueryAndNavigate();
306
358
  return;
307
359
  }
308
360
  case "delete-data-row":
@@ -313,11 +365,57 @@ async function handleAction(actionNode) {
313
365
  return;
314
366
  case "clear-query":
315
367
  clearCurrentQuery();
368
+ if (getState().route.name === "editorResults") {
369
+ router.navigate("/editor");
370
+ }
316
371
  return;
317
372
  case "clear-results":
318
373
  clearEditorResults();
319
374
  router.navigate("/editor");
320
375
  return;
376
+ case "select-query-history-item":
377
+ if (actionNode.dataset.historyId) {
378
+ await selectQueryHistoryItem(actionNode.dataset.historyId);
379
+ }
380
+ return;
381
+ case "clear-query-history-selection":
382
+ clearQueryHistorySelection();
383
+ return;
384
+ case "set-query-history-tab":
385
+ if (actionNode.dataset.tab) {
386
+ await setQueryHistoryTab(actionNode.dataset.tab);
387
+ }
388
+ return;
389
+ case "load-more-query-history":
390
+ await loadMoreQueryHistory();
391
+ return;
392
+ case "open-query-history":
393
+ if (actionNode.dataset.historyId && openQueryHistoryInEditor(actionNode.dataset.historyId)) {
394
+ router.navigate("/editor");
395
+ }
396
+ return;
397
+ case "run-query-history":
398
+ if (actionNode.dataset.historyId) {
399
+ const success = await runQueryHistoryItem(actionNode.dataset.historyId);
400
+ router.navigate(success ? "/editor/results" : "/editor");
401
+ }
402
+ return;
403
+ case "toggle-query-history-saved":
404
+ if (actionNode.dataset.historyId) {
405
+ await toggleQueryHistorySavedState(
406
+ actionNode.dataset.historyId,
407
+ actionNode.dataset.nextValue === "true"
408
+ );
409
+ }
410
+ return;
411
+ case "delete-query-history":
412
+ if (
413
+ actionNode.dataset.historyId &&
414
+ window.confirm("Delete this query history item and all recorded runs?")
415
+ ) {
416
+ await deleteQueryHistoryStateItem(actionNode.dataset.historyId);
417
+ }
418
+ return;
321
419
  case "set-editor-tab": {
322
420
  const tab = actionNode.dataset.tab;
323
421
  if (!tab) {
@@ -327,9 +425,6 @@ async function handleAction(actionNode) {
327
425
  router.navigate(tab === "results" ? "/editor/results" : "/editor");
328
426
  return;
329
427
  }
330
- case "clear-sql-history":
331
- await clearSqlHistoryStateAndData();
332
- return;
333
428
  case "export-query-csv":
334
429
  await exportCurrentQueryCsv();
335
430
  return;
@@ -362,6 +457,16 @@ async function handleAction(actionNode) {
362
457
  await setDataPage(actionNode.dataset.page);
363
458
  }
364
459
  return;
460
+ case "sort-data-column":
461
+ if (actionNode.dataset.columnName) {
462
+ await sortDataTableByColumn(actionNode.dataset.columnName);
463
+ }
464
+ return;
465
+ case "sort-editor-results-column":
466
+ if (actionNode.dataset.columnName) {
467
+ sortEditorResultsByColumn(actionNode.dataset.columnName);
468
+ }
469
+ return;
365
470
  case "set-data-page-size":
366
471
  if (actionNode.dataset.pageSize) {
367
472
  await setDataPageSize(actionNode.dataset.pageSize);
@@ -385,6 +490,27 @@ document.addEventListener("click", (event) => {
385
490
  });
386
491
 
387
492
  document.addEventListener("keydown", (event) => {
493
+ const target = event.target;
494
+
495
+ if (
496
+ event.key === "Enter" &&
497
+ event.shiftKey &&
498
+ !event.altKey &&
499
+ !event.ctrlKey &&
500
+ !event.metaKey &&
501
+ !event.defaultPrevented &&
502
+ target instanceof HTMLTextAreaElement &&
503
+ target.dataset.bind === "current-query"
504
+ ) {
505
+ event.preventDefault();
506
+
507
+ if (!getState().editor.executing) {
508
+ void executeEditorQueryAndNavigate();
509
+ }
510
+
511
+ return;
512
+ }
513
+
388
514
  if (event.key !== "Escape" || event.defaultPrevented) {
389
515
  return;
390
516
  }
@@ -420,6 +546,16 @@ document.addEventListener("input", (event) => {
420
546
  syncQueryEditorHighlight(bindNode);
421
547
  syncQueryEditorScroll(bindNode);
422
548
  setCurrentQuery(bindNode.value);
549
+ return;
550
+ }
551
+
552
+ if (bindNode.dataset.bind === "data-search-query") {
553
+ setDataSearchQuery(bindNode.value);
554
+ return;
555
+ }
556
+
557
+ if (bindNode.dataset.bind === "query-history-search") {
558
+ setQueryHistorySearchInput(bindNode.value);
423
559
  }
424
560
  });
425
561
 
@@ -444,9 +580,8 @@ document.addEventListener("change", (event) => {
444
580
  return;
445
581
  }
446
582
 
447
- if (bindNode.dataset.bind === "history-entry" && bindNode.value) {
448
- loadQueryFromHistory(bindNode.value);
449
- bindNode.value = "";
583
+ if (bindNode.dataset.bind === "data-search-column") {
584
+ setDataSearchColumn(bindNode.value);
450
585
  }
451
586
  });
452
587
 
@@ -512,6 +647,8 @@ document.addEventListener("submit", async (event) => {
512
647
  case "edit-connection": {
513
648
  const connectionId = String(formData.get("connectionId") ?? "");
514
649
  const isActiveConnection = getState().connections.active?.id === connectionId;
650
+ const logoFile = formData.get("logoFile");
651
+ const logoUpload = await buildConnectionLogoUpload(logoFile);
515
652
 
516
653
  if (isActiveConnection) {
517
654
  resetStructureGraphForDatabaseChange();
@@ -521,6 +658,8 @@ document.addEventListener("submit", async (event) => {
521
658
  path: String(formData.get("path") ?? ""),
522
659
  label: String(formData.get("label") ?? ""),
523
660
  readOnly: formData.get("readOnly") === "on",
661
+ clearLogo: formData.get("clearLogo") === "on" && !logoUpload,
662
+ logoUpload,
524
663
  });
525
664
 
526
665
  return;
@@ -562,6 +701,18 @@ document.addEventListener("submit", async (event) => {
562
701
  );
563
702
  return;
564
703
  }
704
+ case "save-query-history-title":
705
+ await saveQueryHistoryTitle(
706
+ String(formData.get("historyId") ?? ""),
707
+ String(formData.get("title") ?? "")
708
+ );
709
+ return;
710
+ case "save-query-history-notes":
711
+ await saveQueryHistoryNotes(
712
+ String(formData.get("historyId") ?? ""),
713
+ String(formData.get("notes") ?? "")
714
+ );
715
+ return;
565
716
  default:
566
717
  }
567
718
  });
@@ -5,6 +5,7 @@ import {
5
5
  truncateMiddle,
6
6
  } from "../utils/format.js";
7
7
  import { renderStatusBadge } from "./badges.js";
8
+ import { renderConnectionLogo } from "./connectionLogo.js";
8
9
 
9
10
  export function renderConnectionCard(connection, activeConnectionId) {
10
11
  const isActive = activeConnectionId
@@ -20,16 +21,14 @@ export function renderConnectionCard(connection, activeConnectionId) {
20
21
  >
21
22
  <div class="flex-1 p-6">
22
23
  <div class="mb-6 flex items-start justify-between">
23
- <div
24
- class="clipped-corner flex h-10 w-10 items-center justify-center transition-colors ${
24
+ ${renderConnectionLogo(connection, {
25
+ containerClass: `clipped-corner flex h-10 w-10 items-center justify-center overflow-hidden transition-colors ${
25
26
  isActive ? "bg-primary-container" : "bg-surface-container-highest"
26
- }"
27
- style="--clip-path: ${clipPath};"
28
- >
29
- <span class="material-symbols-outlined ${
30
- isActive ? "text-on-primary" : "text-outline-variant"
31
- }">database</span>
32
- </div>
27
+ }`,
28
+ containerAttributes: `style="--clip-path: ${clipPath};"`,
29
+ imageClassName: "h-full w-full object-cover",
30
+ iconClassName: isActive ? "text-on-primary" : "text-outline-variant",
31
+ })}
33
32
  <div class="flex items-center gap-2">
34
33
  ${renderStatusBadge(isActive ? "ACTIVE" : "RECENT", isActive ? "primary" : "muted")}
35
34
  ${connection.readOnly ? renderStatusBadge("READ_ONLY", "alert") : ""}
@@ -0,0 +1,33 @@
1
+ import { escapeHtml } from "../utils/format.js";
2
+
3
+ export function renderConnectionLogo(
4
+ connection,
5
+ {
6
+ containerClass = "",
7
+ containerAttributes = "",
8
+ imageClassName = "h-full w-full object-cover",
9
+ iconClassName = "",
10
+ icon = "database",
11
+ } = {}
12
+ ) {
13
+ const logoUrl = connection?.logoUrl ?? null;
14
+ const label = connection?.label ?? "Database";
15
+
16
+ if (logoUrl) {
17
+ return `
18
+ <div ${containerAttributes} class="${containerClass}">
19
+ <img
20
+ alt="${escapeHtml(`${label} logo`)}"
21
+ class="${imageClassName}"
22
+ src="${escapeHtml(logoUrl)}"
23
+ />
24
+ </div>
25
+ `;
26
+ }
27
+
28
+ return `
29
+ <div ${containerAttributes} class="${containerClass}">
30
+ <span class="material-symbols-outlined ${iconClassName}">${escapeHtml(icon)}</span>
31
+ </div>
32
+ `;
33
+ }
@@ -14,9 +14,9 @@ export function renderDataGrid({
14
14
  <tr class="${headerRowClass}">
15
15
  ${columns
16
16
  .map(
17
- (column) => `
18
- <th class="${column.headerClassName ?? ""}">
19
- ${column.label}
17
+ (column) => `
18
+ <th class="${column.headerClassName ?? ""}" ${column.headerAttrs ?? ""}>
19
+ ${column.renderHeader ? column.renderHeader() : column.label}
20
20
  </th>
21
21
  `
22
22
  )
@@ -4,6 +4,7 @@ import {
4
4
  formatDateTime,
5
5
  truncateMiddle,
6
6
  } from "../utils/format.js";
7
+ import { renderConnectionLogo } from "./connectionLogo.js";
7
8
 
8
9
  function renderRecentConnections(recentConnections = []) {
9
10
  if (!recentConnections.length) {
@@ -26,7 +27,12 @@ function renderRecentConnections(recentConnections = []) {
26
27
  data-connection-id="${escapeHtml(connection.id)}"
27
28
  type="button"
28
29
  >
29
- <span class="material-symbols-outlined text-sm text-primary-container">database</span>
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
+ })}
30
36
  <span class="min-w-0">
31
37
  <span class="block truncate font-mono text-xs">${escapeHtml(connection.label)}</span>
32
38
  <span class="block truncate text-[10px] text-on-surface-variant/45">${escapeHtml(
@@ -53,16 +59,24 @@ function renderActiveConnection(activeConnection) {
53
59
  return `
54
60
  <div class="mx-auto max-w-2xl border border-outline-variant/15 bg-surface-container-low px-6 py-5 text-left">
55
61
  <div class="flex flex-wrap items-start justify-between gap-4">
56
- <div>
57
- <p class="text-[10px] font-mono uppercase tracking-[0.24em] text-primary-container/70">
58
- ACTIVE_DATABASE
59
- </p>
60
- <h2 class="mt-2 font-headline text-2xl font-black uppercase tracking-tight text-primary-container">
61
- ${escapeHtml(activeConnection.label)}
62
- </h2>
63
- <p class="mt-2 font-mono text-[10px] text-on-surface-variant/55">${escapeHtml(
64
- truncateMiddle(activeConnection.path, 72)
65
- )}</p>
62
+ <div class="flex items-start gap-4">
63
+ ${renderConnectionLogo(activeConnection, {
64
+ containerClass:
65
+ "flex h-14 w-14 items-center justify-center overflow-hidden border border-outline-variant/15 bg-surface-container-highest",
66
+ imageClassName: "h-full w-full object-cover",
67
+ iconClassName: "text-2xl text-primary-container",
68
+ })}
69
+ <div>
70
+ <p class="text-[10px] font-mono uppercase tracking-[0.24em] text-primary-container/70">
71
+ ACTIVE_DATABASE
72
+ </p>
73
+ <h2 class="mt-2 font-headline text-2xl font-black uppercase tracking-tight text-primary-container">
74
+ ${escapeHtml(activeConnection.label)}
75
+ </h2>
76
+ <p class="mt-2 font-mono text-[10px] text-on-surface-variant/55">${escapeHtml(
77
+ truncateMiddle(activeConnection.path, 72)
78
+ )}</p>
79
+ </div>
66
80
  </div>
67
81
  <div class="text-right text-xs text-on-surface-variant/65">
68
82
  <div>${escapeHtml(formatBytes(activeConnection.sizeBytes))}</div>