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
|
@@ -5,6 +5,10 @@ const DATE_TIME_FORMATTER = new Intl.DateTimeFormat("en-GB", {
|
|
|
5
5
|
dateStyle: "medium",
|
|
6
6
|
timeStyle: "short",
|
|
7
7
|
});
|
|
8
|
+
const COMPACT_DATE_TIME_FORMATTER = new Intl.DateTimeFormat("en-GB", {
|
|
9
|
+
dateStyle: "short",
|
|
10
|
+
timeStyle: "short",
|
|
11
|
+
});
|
|
8
12
|
|
|
9
13
|
export function escapeHtml(value = "") {
|
|
10
14
|
return String(value)
|
|
@@ -52,6 +56,25 @@ export function formatDateTime(value) {
|
|
|
52
56
|
return Number.isNaN(date.getTime()) ? String(value) : DATE_TIME_FORMATTER.format(date);
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
export function formatCompactDateTime(value) {
|
|
60
|
+
if (!value) {
|
|
61
|
+
return "N/A";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const date = new Date(value);
|
|
65
|
+
return Number.isNaN(date.getTime()) ? String(value) : COMPACT_DATE_TIME_FORMATTER.format(date);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function formatDurationMs(value) {
|
|
69
|
+
const numericValue = Number(value);
|
|
70
|
+
|
|
71
|
+
if (!Number.isFinite(numericValue) || numericValue < 0) {
|
|
72
|
+
return "N/A";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return `${numericValue.toLocaleString("en-US")} ms`;
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
export function formatRelativeBoolean(value) {
|
|
56
79
|
return value ? "ENABLED" : "DISABLED";
|
|
57
80
|
}
|
|
@@ -168,6 +168,110 @@ function getCellWidthClass(columnName) {
|
|
|
168
168
|
return "max-w-[12rem]";
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
function getSortIcon(columnName, sortColumn, sortDirection) {
|
|
172
|
+
if (columnName !== sortColumn) {
|
|
173
|
+
return "unfold_more";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return sortDirection === "desc" ? "south" : "north";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function renderSortableHeader(columnName, sortColumn, sortDirection, action) {
|
|
180
|
+
const isActive = columnName === sortColumn;
|
|
181
|
+
|
|
182
|
+
return `
|
|
183
|
+
<button
|
|
184
|
+
class="flex w-full items-center justify-between gap-2 text-left transition-colors ${
|
|
185
|
+
isActive ? "text-primary-container" : "text-on-surface-variant hover:text-primary-container"
|
|
186
|
+
}"
|
|
187
|
+
data-action="${action}"
|
|
188
|
+
data-column-name="${escapeHtml(columnName)}"
|
|
189
|
+
type="button"
|
|
190
|
+
>
|
|
191
|
+
<span class="truncate">${escapeHtml(columnName)}</span>
|
|
192
|
+
<span class="material-symbols-outlined text-sm leading-none">${getSortIcon(
|
|
193
|
+
columnName,
|
|
194
|
+
sortColumn,
|
|
195
|
+
sortDirection
|
|
196
|
+
)}</span>
|
|
197
|
+
</button>
|
|
198
|
+
`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function getFilteredTableRows(table, state) {
|
|
202
|
+
const allRows = table?.rows ?? [];
|
|
203
|
+
const availableColumns = table?.columns ?? [];
|
|
204
|
+
const searchQuery = String(state.dataBrowser.searchQuery ?? "").trim().toLowerCase();
|
|
205
|
+
const activeColumn = availableColumns.includes(state.dataBrowser.searchColumn)
|
|
206
|
+
? state.dataBrowser.searchColumn
|
|
207
|
+
: (availableColumns[0] ?? "");
|
|
208
|
+
|
|
209
|
+
const indexedRows = allRows.map((row, index) => ({
|
|
210
|
+
row,
|
|
211
|
+
index,
|
|
212
|
+
}));
|
|
213
|
+
|
|
214
|
+
if (!searchQuery || !activeColumn) {
|
|
215
|
+
return {
|
|
216
|
+
activeColumn,
|
|
217
|
+
filteredRows: indexedRows,
|
|
218
|
+
searchQuery,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
activeColumn,
|
|
224
|
+
searchQuery,
|
|
225
|
+
filteredRows: indexedRows.filter(({ row }) =>
|
|
226
|
+
formatCellValue(row[activeColumn]).toLowerCase().includes(searchQuery)
|
|
227
|
+
),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function renderTableSearchBar(table, state, activeColumn, filteredRowCount) {
|
|
232
|
+
const columns = table?.columns ?? [];
|
|
233
|
+
|
|
234
|
+
if (!table || !columns.length) {
|
|
235
|
+
return "";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return `
|
|
239
|
+
<div class="flex flex-wrap items-center gap-3 border-b border-outline-variant/10 bg-surface-container-low px-6 py-4">
|
|
240
|
+
<label class="flex min-w-[18rem] flex-1 items-center gap-3 border border-outline-variant/20 bg-surface-container-lowest px-4 py-3">
|
|
241
|
+
<span class="material-symbols-outlined text-base text-on-surface-variant/55">search</span>
|
|
242
|
+
<input
|
|
243
|
+
class="min-w-0 flex-1 bg-transparent text-sm text-on-surface outline-none placeholder:text-on-surface-variant/40"
|
|
244
|
+
data-bind="data-search-query"
|
|
245
|
+
placeholder="Filter current page..."
|
|
246
|
+
type="search"
|
|
247
|
+
value="${escapeHtml(state.dataBrowser.searchQuery ?? "")}"
|
|
248
|
+
/>
|
|
249
|
+
</label>
|
|
250
|
+
<select
|
|
251
|
+
class="min-w-[14rem] border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 font-mono text-xs tracking-[0.04em] text-on-surface outline-none"
|
|
252
|
+
data-bind="data-search-column"
|
|
253
|
+
>
|
|
254
|
+
${columns
|
|
255
|
+
.map(
|
|
256
|
+
(columnName) => `
|
|
257
|
+
<option value="${escapeHtml(columnName)}" ${
|
|
258
|
+
columnName === activeColumn ? "selected" : ""
|
|
259
|
+
}>
|
|
260
|
+
${escapeHtml(columnName)}
|
|
261
|
+
</option>
|
|
262
|
+
`
|
|
263
|
+
)
|
|
264
|
+
.join("")}
|
|
265
|
+
</select>
|
|
266
|
+
<div class="text-[10px] font-mono tracking-[0.14em] text-on-surface-variant/55">
|
|
267
|
+
${escapeHtml(formatNumber(filteredRowCount))} match${
|
|
268
|
+
filteredRowCount === 1 ? "" : "es"
|
|
269
|
+
} on this page
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
|
|
171
275
|
function renderTableSurface(state) {
|
|
172
276
|
const table = state.dataBrowser.table;
|
|
173
277
|
|
|
@@ -197,10 +301,14 @@ function renderTableSurface(state) {
|
|
|
197
301
|
`;
|
|
198
302
|
}
|
|
199
303
|
|
|
304
|
+
const { activeColumn, filteredRows, searchQuery } = getFilteredTableRows(table, state);
|
|
305
|
+
const sortColumn = state.dataBrowser.sortColumn;
|
|
306
|
+
const sortDirection = state.dataBrowser.sortDirection;
|
|
200
307
|
const columns = (table.columns ?? []).map((columnName) => ({
|
|
201
|
-
label: escapeHtml(columnName),
|
|
202
308
|
headerClassName:
|
|
203
|
-
"border-b border-primary-container/20 px-4 py-3 text-[10px] font-bold
|
|
309
|
+
"border-b border-primary-container/20 px-4 py-3 text-[10px] font-bold tracking-[0.08em] text-primary-container",
|
|
310
|
+
renderHeader: () =>
|
|
311
|
+
renderSortableHeader(columnName, sortColumn, sortDirection, "sort-data-column"),
|
|
204
312
|
cellClassName: "px-4 py-2 align-top text-[11px] text-on-surface",
|
|
205
313
|
render: (row) => {
|
|
206
314
|
const value = formatCellValue(row[columnName]);
|
|
@@ -219,25 +327,35 @@ function renderTableSurface(state) {
|
|
|
219
327
|
const fromRow = totalRows === 0 ? 0 : (table.offset ?? 0) + 1;
|
|
220
328
|
const toRow = totalRows === 0 ? 0 : Math.min((table.offset ?? 0) + (table.rows?.length ?? 0), totalRows);
|
|
221
329
|
const pageSizes = [25, 50, 100];
|
|
330
|
+
const filteredRowCount = filteredRows.length;
|
|
331
|
+
const hasActiveSearch = Boolean(searchQuery);
|
|
222
332
|
|
|
223
333
|
return `
|
|
224
334
|
<div class="flex flex-1 min-h-0 flex-col bg-surface-container-lowest">
|
|
335
|
+
${renderTableSearchBar(table, state, activeColumn, filteredRowCount)}
|
|
225
336
|
<div class="custom-scrollbar flex-1 overflow-auto">
|
|
226
337
|
${renderDataGrid({
|
|
227
338
|
columns,
|
|
228
|
-
rows:
|
|
339
|
+
rows: filteredRows.map(({ row }) => row),
|
|
229
340
|
tableClass: "min-w-full border-collapse text-left font-mono text-xs",
|
|
230
341
|
theadClass: "sticky top-0 z-10 bg-surface-container-highest",
|
|
231
342
|
tbodyClass: "divide-y divide-outline-variant/5",
|
|
232
|
-
getRowClass: (_,
|
|
233
|
-
|
|
234
|
-
|
|
343
|
+
getRowClass: (_, filteredIndex) => {
|
|
344
|
+
const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
|
|
345
|
+
|
|
346
|
+
return `${
|
|
347
|
+
state.dataBrowser.selectedRowIndex === rowIndex
|
|
235
348
|
? "bg-surface-bright"
|
|
236
|
-
:
|
|
349
|
+
: filteredIndex % 2 === 0
|
|
237
350
|
? "bg-surface-container-low"
|
|
238
351
|
: "bg-surface-container-lowest"
|
|
239
|
-
} cursor-pointer transition-colors hover:bg-surface-container-high
|
|
240
|
-
|
|
352
|
+
} cursor-pointer transition-colors hover:bg-surface-container-high`;
|
|
353
|
+
},
|
|
354
|
+
getRowAttrs: (_, filteredIndex) => {
|
|
355
|
+
const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
|
|
356
|
+
|
|
357
|
+
return `data-action="select-data-row" data-row-index="${rowIndex}"`;
|
|
358
|
+
},
|
|
241
359
|
})}
|
|
242
360
|
${
|
|
243
361
|
!table.rows?.length
|
|
@@ -248,6 +366,14 @@ function renderTableSurface(state) {
|
|
|
248
366
|
</p>
|
|
249
367
|
</div>
|
|
250
368
|
`
|
|
369
|
+
: !filteredRowCount
|
|
370
|
+
? `
|
|
371
|
+
<div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10">
|
|
372
|
+
<p class="font-mono text-[10px] tracking-[0.18em] text-on-surface-variant/40">
|
|
373
|
+
${hasActiveSearch ? "No matching rows on this page." : "No rows available."}
|
|
374
|
+
</p>
|
|
375
|
+
</div>
|
|
376
|
+
`
|
|
251
377
|
: ""
|
|
252
378
|
}
|
|
253
379
|
</div>
|
|
@@ -255,7 +381,7 @@ function renderTableSurface(state) {
|
|
|
255
381
|
<div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
256
382
|
showing ${escapeHtml(formatNumber(fromRow))}-${escapeHtml(formatNumber(toRow))} of ${escapeHtml(
|
|
257
383
|
formatNumber(totalRows)
|
|
258
|
-
)} rows
|
|
384
|
+
)} rows${hasActiveSearch ? ` // ${escapeHtml(formatNumber(filteredRowCount))} visible on this page` : ""}
|
|
259
385
|
</div>
|
|
260
386
|
<div class="flex flex-wrap items-center gap-4">
|
|
261
387
|
<div class="flex items-center gap-2">
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { renderBottomTabs } from "../components/bottomTabs.js";
|
|
2
2
|
import { renderQueryEditor } from "../components/queryEditor.js";
|
|
3
|
+
import { renderQueryHistoryDetail } from "../components/queryHistoryDetail.js";
|
|
4
|
+
import { renderQueryHistoryPanel } from "../components/queryHistoryPanel.js";
|
|
3
5
|
import { renderRowEditorPanel } from "../components/rowEditorPanel.js";
|
|
4
6
|
import { renderQueryResultsPane } from "../components/queryResults.js";
|
|
5
7
|
import { getCurrentConnection, getQueryMessages, getQueryPerformance } from "../store.js";
|
|
@@ -216,17 +218,17 @@ function renderResultsSurface(state, isResultsRoute) {
|
|
|
216
218
|
|
|
217
219
|
content = state.connections.active
|
|
218
220
|
? renderQueryResultsPane(state.editor.result, {
|
|
219
|
-
exporting: state.editor.exportLoading,
|
|
220
221
|
selectedRowIndex: state.editor.selectedRowIndex,
|
|
221
222
|
editable: editingState.enabled,
|
|
222
|
-
|
|
223
|
+
sortColumn: state.editor.resultSortColumn,
|
|
224
|
+
sortDirection: state.editor.resultSortDirection,
|
|
223
225
|
})
|
|
224
226
|
: renderMissingDatabase();
|
|
225
227
|
}
|
|
226
228
|
|
|
227
229
|
return `
|
|
228
230
|
<div class="flex h-full min-h-0 flex-col border-t border-outline-variant/10 bg-surface-container-lowest">
|
|
229
|
-
${renderBottomTabs(activeTab, counts)}
|
|
231
|
+
${renderBottomTabs(state.editor.activeTab, counts)}
|
|
230
232
|
<div class="min-h-0 flex-1">${content}</div>
|
|
231
233
|
</div>
|
|
232
234
|
`;
|
|
@@ -234,13 +236,13 @@ function renderResultsSurface(state, isResultsRoute) {
|
|
|
234
236
|
|
|
235
237
|
export function renderEditorView(state, { isResultsRoute = false } = {}) {
|
|
236
238
|
const connection = getCurrentConnection(state);
|
|
237
|
-
const editorSectionClass =
|
|
238
|
-
const resultsSectionClass =
|
|
239
|
+
const editorSectionClass = "min-h-[27.5%]";
|
|
240
|
+
const resultsSectionClass = "flex-1";
|
|
239
241
|
|
|
240
242
|
return {
|
|
241
243
|
main: `
|
|
242
|
-
<section class="view-surface flex h-full min-h-0 flex-col overflow-hidden">
|
|
243
|
-
<div class="flex h-
|
|
244
|
+
<section class="view-surface flex h-full min-h-0 flex-col overflow-hidden xl:flex-row">
|
|
245
|
+
<div class="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
|
244
246
|
<section class="${editorSectionClass} flex min-h-0 flex-col ${
|
|
245
247
|
isResultsRoute ? "border-b-4 border-background" : ""
|
|
246
248
|
}">
|
|
@@ -248,17 +250,39 @@ export function renderEditorView(state, { isResultsRoute = false } = {}) {
|
|
|
248
250
|
query: state.editor.sqlText,
|
|
249
251
|
executing: state.editor.executing,
|
|
250
252
|
exporting: state.editor.exportLoading,
|
|
251
|
-
history: state.editor.history,
|
|
252
253
|
historyLoading: state.editor.historyLoading,
|
|
254
|
+
historyTotal: state.editor.historyTotal,
|
|
253
255
|
title: connection?.label ?? "SQLite Query Workspace",
|
|
254
256
|
})}
|
|
255
257
|
</section>
|
|
256
|
-
<section class="${resultsSectionClass} flex min-h-0 flex-col overflow-hidden">
|
|
258
|
+
<section class="${resultsSectionClass} flex min-h-0 min-w-0 flex-col overflow-hidden">
|
|
257
259
|
${renderResultsSurface(state, isResultsRoute)}
|
|
258
260
|
</section>
|
|
259
261
|
</div>
|
|
262
|
+
${renderQueryHistoryPanel({
|
|
263
|
+
items: state.editor.history,
|
|
264
|
+
loading: state.editor.historyLoading,
|
|
265
|
+
loadingMore: state.editor.historyLoadingMore,
|
|
266
|
+
error: state.editor.historyError,
|
|
267
|
+
activeTab: state.editor.historyTab,
|
|
268
|
+
search: state.editor.historySearchInput,
|
|
269
|
+
total: state.editor.historyTotal,
|
|
270
|
+
hasMore: state.editor.historyHasMore,
|
|
271
|
+
activeHistoryId: state.editor.historyActiveId,
|
|
272
|
+
selectedHistoryId: state.editor.historySelectedId,
|
|
273
|
+
})}
|
|
260
274
|
</section>
|
|
261
275
|
`,
|
|
262
|
-
panel:
|
|
276
|
+
panel:
|
|
277
|
+
isResultsRoute && state.editor.selectedRowIndex !== null
|
|
278
|
+
? renderEditorRowPanel(state)
|
|
279
|
+
: state.editor.historySelectedId
|
|
280
|
+
? renderQueryHistoryDetail({
|
|
281
|
+
item: state.editor.historyDetail,
|
|
282
|
+
runs: state.editor.historyRuns,
|
|
283
|
+
loading: state.editor.historyDetailLoading,
|
|
284
|
+
error: state.editor.historyDetailError,
|
|
285
|
+
})
|
|
286
|
+
: "",
|
|
263
287
|
};
|
|
264
288
|
}
|
|
@@ -126,6 +126,21 @@ function renderOperationalSurface(overview) {
|
|
|
126
126
|
)
|
|
127
127
|
.join("")}
|
|
128
128
|
</div>
|
|
129
|
+
${
|
|
130
|
+
overview.file?.path
|
|
131
|
+
? `
|
|
132
|
+
<div class="border-t border-outline-variant/10 px-4 py-3">
|
|
133
|
+
<button
|
|
134
|
+
class="toolbar-button border border-outline-variant/20 bg-surface-container px-3 py-1.5 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
|
|
135
|
+
data-action="open-overview-in-finder"
|
|
136
|
+
type="button"
|
|
137
|
+
>
|
|
138
|
+
Open In Finder
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
`
|
|
142
|
+
: ""
|
|
143
|
+
}
|
|
129
144
|
</section>
|
|
130
145
|
`;
|
|
131
146
|
}
|
|
@@ -6,8 +6,8 @@ function renderEntryGroup(title, entries, activeName, options = {}) {
|
|
|
6
6
|
const { compact = false, showMeta = true } = options;
|
|
7
7
|
|
|
8
8
|
return `
|
|
9
|
-
<section class="shell-section p-5">
|
|
10
|
-
<div class="mb-4 text-[10px] font-bold uppercase tracking-[0.25em] text-primary-container">
|
|
9
|
+
<section class="shell-section flex flex-col p-5">
|
|
10
|
+
<div class="mb-4 shrink-0 text-[10px] font-bold uppercase tracking-[0.25em] text-primary-container">
|
|
11
11
|
${escapeHtml(title)}
|
|
12
12
|
</div>
|
|
13
13
|
${
|
|
@@ -209,10 +209,6 @@ function renderGraphSurface(structure, selectedName, detail, detailLoading) {
|
|
|
209
209
|
type="search"
|
|
210
210
|
/>
|
|
211
211
|
</label>
|
|
212
|
-
<div class="structure-graph__toolbar-meta">
|
|
213
|
-
${escapeHtml(formatNumber(graph.tables?.length ?? 0))} TABLES //
|
|
214
|
-
${escapeHtml(formatNumber(graph.relationshipCount ?? 0))} RELATIONSHIPS
|
|
215
|
-
</div>
|
|
216
212
|
</div>
|
|
217
213
|
<div class="structure-graph__toolbar-actions">
|
|
218
214
|
<button
|
|
@@ -293,8 +289,8 @@ export function renderStructureView(state) {
|
|
|
293
289
|
|
|
294
290
|
return {
|
|
295
291
|
main: `
|
|
296
|
-
<section class="view-surface min-h-
|
|
297
|
-
<div class="view-frame
|
|
292
|
+
<section class="view-surface flex h-full min-h-0 flex-col bg-surface-container">
|
|
293
|
+
<div class="view-frame flex h-full min-h-0 flex-col">
|
|
298
294
|
${renderPageHeader({
|
|
299
295
|
title: "Structure",
|
|
300
296
|
subtitle: "Schema graph, foreign-key paths, raw DDL, and object metadata",
|
|
@@ -303,7 +299,7 @@ export function renderStructureView(state) {
|
|
|
303
299
|
${
|
|
304
300
|
state.structure.loading && !structure
|
|
305
301
|
? `
|
|
306
|
-
<div class="flex min-h-
|
|
302
|
+
<div class="flex min-h-0 flex-1 items-center justify-center border border-outline-variant/10 bg-surface-container-low">
|
|
307
303
|
<div class="text-center text-on-surface-variant/40">
|
|
308
304
|
<span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
|
|
309
305
|
<p class="font-mono text-[10px] uppercase tracking-[0.22em]">LOADING_STRUCTURE</p>
|
|
@@ -312,7 +308,7 @@ export function renderStructureView(state) {
|
|
|
312
308
|
`
|
|
313
309
|
: state.structure.error
|
|
314
310
|
? `
|
|
315
|
-
<div class="border border-error/20 bg-error-container/10 px-6 py-5 text-sm text-on-surface">
|
|
311
|
+
<div class="min-h-0 flex-1 border border-error/20 bg-error-container/10 px-6 py-5 text-sm text-on-surface">
|
|
316
312
|
<div class="font-headline text-xs font-bold uppercase tracking-[0.18em] text-error">
|
|
317
313
|
${escapeHtml(state.structure.error.code)}
|
|
318
314
|
</div>
|
|
@@ -321,8 +317,9 @@ export function renderStructureView(state) {
|
|
|
321
317
|
`
|
|
322
318
|
: structure
|
|
323
319
|
? `
|
|
324
|
-
<section class="grid grid-cols-1 gap-6 xl:grid-cols-[18.5rem_minmax(0,1fr)] 2xl:grid-cols-[19.5rem_minmax(0,1fr)]">
|
|
325
|
-
<div class="
|
|
320
|
+
<section class="grid min-h-0 flex-1 grid-cols-1 gap-6 xl:grid-cols-[18.5rem_minmax(0,1fr)] 2xl:grid-cols-[19.5rem_minmax(0,1fr)]">
|
|
321
|
+
<div class="custom-scrollbar min-h-0 overflow-y-auto pr-1">
|
|
322
|
+
<div class="space-y-6">
|
|
326
323
|
${renderEntryGroup(
|
|
327
324
|
"Tables",
|
|
328
325
|
structure.grouped.tables,
|
|
@@ -345,6 +342,7 @@ export function renderStructureView(state) {
|
|
|
345
342
|
structure.grouped.triggers,
|
|
346
343
|
state.structure.selectedName
|
|
347
344
|
)}
|
|
345
|
+
</div>
|
|
348
346
|
</div>
|
|
349
347
|
${renderGraphSurface(
|
|
350
348
|
structure,
|
|
@@ -316,6 +316,112 @@
|
|
|
316
316
|
padding: var(--spacing-3) var(--spacing-4);
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
.query-history-panel {
|
|
320
|
+
display: flex;
|
|
321
|
+
flex: 0 0 360px;
|
|
322
|
+
flex-direction: column;
|
|
323
|
+
min-height: 0;
|
|
324
|
+
width: 360px;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.query-history-item {
|
|
328
|
+
align-items: stretch;
|
|
329
|
+
background: var(--color-surface-low);
|
|
330
|
+
border: 1px solid rgba(75, 71, 50, 0.16);
|
|
331
|
+
border-left: 2px solid transparent;
|
|
332
|
+
display: flex;
|
|
333
|
+
flex-direction: column;
|
|
334
|
+
min-width: 0;
|
|
335
|
+
transition: background-color var(--transition-fast), border-color var(--transition-fast),
|
|
336
|
+
box-shadow var(--transition-fast);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.query-history-item:hover {
|
|
340
|
+
background: var(--color-surface-high);
|
|
341
|
+
border-color: rgba(252, 227, 0, 0.18);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.query-history-item.is-active {
|
|
345
|
+
border-left-color: var(--color-primary-container);
|
|
346
|
+
box-shadow: 0 0 24px rgba(252, 227, 0, 0.06);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.query-history-item.is-error {
|
|
350
|
+
border-left-color: var(--color-error);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.query-history-item-hit {
|
|
354
|
+
background: transparent;
|
|
355
|
+
border: 0;
|
|
356
|
+
color: inherit;
|
|
357
|
+
display: block;
|
|
358
|
+
min-width: 0;
|
|
359
|
+
padding: var(--spacing-3);
|
|
360
|
+
text-align: left;
|
|
361
|
+
transition: background-color var(--transition-fast);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.query-history-item-hit.is-active {
|
|
365
|
+
background: rgba(252, 227, 0, 0.08);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.query-history-icon-button {
|
|
369
|
+
align-items: center;
|
|
370
|
+
background: transparent;
|
|
371
|
+
border: 0;
|
|
372
|
+
color: rgba(229, 226, 225, 0.6);
|
|
373
|
+
display: inline-flex;
|
|
374
|
+
height: 2rem;
|
|
375
|
+
justify-content: center;
|
|
376
|
+
transition: color var(--transition-fast), background-color var(--transition-fast);
|
|
377
|
+
width: 2rem;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.query-history-icon-button:hover,
|
|
381
|
+
.query-history-icon-button.is-active {
|
|
382
|
+
background: var(--color-surface-highest);
|
|
383
|
+
color: var(--color-primary-container);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.query-history-tab {
|
|
387
|
+
background: transparent;
|
|
388
|
+
border: 0;
|
|
389
|
+
border-bottom: 1px solid transparent;
|
|
390
|
+
color: rgba(205, 199, 171, 0.45);
|
|
391
|
+
font-family: var(--font-family-mono);
|
|
392
|
+
font-size: var(--font-size-status);
|
|
393
|
+
letter-spacing: 0.16em;
|
|
394
|
+
padding: 0.35rem 0;
|
|
395
|
+
text-transform: uppercase;
|
|
396
|
+
transition: border-color var(--transition-fast), color var(--transition-fast);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.query-history-tab.is-active {
|
|
400
|
+
border-color: var(--color-primary-container);
|
|
401
|
+
color: var(--color-primary-container);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.query-history-sql-preview {
|
|
405
|
+
display: -webkit-box;
|
|
406
|
+
-webkit-box-orient: vertical;
|
|
407
|
+
-webkit-line-clamp: 2;
|
|
408
|
+
overflow: hidden;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.query-history-detail-sql {
|
|
412
|
+
max-height: 240px;
|
|
413
|
+
white-space: pre-wrap;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
@media (max-width: 1279px) {
|
|
417
|
+
.query-history-panel {
|
|
418
|
+
border-left: 0;
|
|
419
|
+
border-top: 1px solid rgba(75, 71, 50, 0.16);
|
|
420
|
+
flex: 0 0 auto;
|
|
421
|
+
width: 100%;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
319
425
|
@media (min-width: 768px) {
|
|
320
426
|
.top-nav-links {
|
|
321
427
|
display: flex;
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
border: 1px solid rgba(138, 123, 52, 0.26);
|
|
4
4
|
display: flex;
|
|
5
5
|
flex-direction: column;
|
|
6
|
-
|
|
6
|
+
height: 100%;
|
|
7
|
+
min-height: 0;
|
|
7
8
|
overflow: hidden;
|
|
8
9
|
}
|
|
9
10
|
|
|
@@ -100,14 +101,6 @@
|
|
|
100
101
|
appearance: none;
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
.structure-graph__toolbar-meta {
|
|
104
|
-
color: rgba(231, 223, 189, 0.64);
|
|
105
|
-
font-family: var(--font-family-mono);
|
|
106
|
-
font-size: 0.625rem;
|
|
107
|
-
letter-spacing: 0.18em;
|
|
108
|
-
text-transform: uppercase;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
104
|
.structure-graph__button {
|
|
112
105
|
align-items: center;
|
|
113
106
|
background: rgba(17, 17, 15, 0.94);
|
|
@@ -193,8 +186,10 @@
|
|
|
193
186
|
.structure-graph__inspector {
|
|
194
187
|
background: linear-gradient(to bottom, rgba(26, 25, 23, 0.98), rgba(11, 11, 10, 0.99));
|
|
195
188
|
border-left: 1px solid rgba(138, 123, 52, 0.24);
|
|
189
|
+
height: 100%;
|
|
196
190
|
min-height: 0;
|
|
197
|
-
overflow: auto;
|
|
191
|
+
overflow-y: auto;
|
|
192
|
+
overflow-x: hidden;
|
|
198
193
|
}
|
|
199
194
|
|
|
200
195
|
.structure-graph.is-inspector-hidden .structure-graph__inspector {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sqlite-hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "SQLite-only local management app backend and SPA shell",
|
|
5
5
|
"main": "server/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/sqlite-hub.js",
|
|
11
11
|
"dev": "node --watch server/server.js --port:4180",
|
|
12
|
-
"publish": "bash publish_brew.sh && bash publish_npm.sh"
|
|
12
|
+
"publish": "bash scripts/publish_brew.sh && sleep 10 && npm login --auth-type=web && bash scripts/publish_npm.sh"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"better-sqlite3": "^11.8.1",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
set -euo pipefail
|
|
4
4
|
|
|
5
|
-
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
6
6
|
cd "$ROOT_DIR"
|
|
7
7
|
|
|
8
8
|
DRY_RUN=0
|
|
@@ -17,7 +17,7 @@ GH_REMOTE="origin"
|
|
|
17
17
|
usage() {
|
|
18
18
|
cat <<'EOF'
|
|
19
19
|
Usage:
|
|
20
|
-
./
|
|
20
|
+
./scripts/publish_brew.sh [options]
|
|
21
21
|
|
|
22
22
|
Options:
|
|
23
23
|
--version X.Y.Z Override the package.json version for this publish run.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
set -euo pipefail
|
|
4
4
|
|
|
5
|
-
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
6
6
|
cd "$ROOT_DIR"
|
|
7
7
|
|
|
8
8
|
DRY_RUN=0
|
|
@@ -15,7 +15,7 @@ GH_REMOTE="origin"
|
|
|
15
15
|
usage() {
|
|
16
16
|
cat <<'EOF'
|
|
17
17
|
Usage:
|
|
18
|
-
./publish_npm.sh [options]
|
|
18
|
+
./scripts/publish_npm.sh [options]
|
|
19
19
|
|
|
20
20
|
Options:
|
|
21
21
|
--tag NAME Publish to a custom npm dist-tag instead of `latest`.
|
|
File without changes
|
|
@@ -113,6 +113,8 @@ function createConnectionsRouter({ connectionManager, importService, backupServi
|
|
|
113
113
|
filePath: req.body.path,
|
|
114
114
|
label: req.body.label,
|
|
115
115
|
readOnly: Boolean(req.body.readOnly),
|
|
116
|
+
logoUpload: req.body.logoUpload ?? null,
|
|
117
|
+
clearLogo: Boolean(req.body.clearLogo),
|
|
116
118
|
});
|
|
117
119
|
|
|
118
120
|
res.json(
|
package/server/routes/data.js
CHANGED
|
@@ -26,6 +26,8 @@ function createDataRouter({ dataBrowserService }) {
|
|
|
26
26
|
const data = dataBrowserService.getTableData(req.params.tableName, {
|
|
27
27
|
limit: req.query.limit,
|
|
28
28
|
offset: req.query.offset,
|
|
29
|
+
sortColumn: req.query.sortColumn,
|
|
30
|
+
sortDirection: req.query.sortDirection,
|
|
29
31
|
});
|
|
30
32
|
|
|
31
33
|
res.json(
|
package/server/routes/export.js
CHANGED
|
@@ -24,7 +24,10 @@ function createExportRouter({ exportService }) {
|
|
|
24
24
|
router.post(
|
|
25
25
|
"/table.csv",
|
|
26
26
|
route((req, res) => {
|
|
27
|
-
const result = exportService.exportTable(req.body?.tableName
|
|
27
|
+
const result = exportService.exportTable(req.body?.tableName, {
|
|
28
|
+
sortColumn: req.body?.sortColumn,
|
|
29
|
+
sortDirection: req.body?.sortDirection,
|
|
30
|
+
});
|
|
28
31
|
sendCsv(res, result);
|
|
29
32
|
})
|
|
30
33
|
);
|
|
@@ -31,6 +31,18 @@ function createOverviewRouter({ overviewService }) {
|
|
|
31
31
|
})
|
|
32
32
|
);
|
|
33
33
|
|
|
34
|
+
router.post(
|
|
35
|
+
"/overview/open-in-finder",
|
|
36
|
+
route(async (req, res) => {
|
|
37
|
+
await overviewService.revealActiveDatabaseInFinder();
|
|
38
|
+
res.json(
|
|
39
|
+
successResponse({
|
|
40
|
+
message: "Database file revealed in Finder.",
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
|
|
34
46
|
return router;
|
|
35
47
|
}
|
|
36
48
|
|