sqlite-hub 0.1.3
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/.npmingnore +4 -0
- package/README.md +46 -0
- package/assets/images/logo.webp +0 -0
- package/assets/images/logo_extrasmall.webp +0 -0
- package/assets/images/logo_raw.png +0 -0
- package/assets/images/logo_small.webp +0 -0
- package/assets/mockups/connections.png +0 -0
- package/assets/mockups/data.png +0 -0
- package/assets/mockups/data_edit.png +0 -0
- package/assets/mockups/home.png +0 -0
- package/assets/mockups/overview.png +0 -0
- package/assets/mockups/sql_editor.png +0 -0
- package/assets/mockups/structure.png +0 -0
- package/bin/sqlite-hub.js +116 -0
- package/changelog.md +3 -0
- package/data/.gitkeep +0 -0
- package/index.html +100 -0
- package/js/api.js +193 -0
- package/js/app.js +520 -0
- package/js/components/actionBar.js +8 -0
- package/js/components/appShell.js +17 -0
- package/js/components/badges.js +5 -0
- package/js/components/bottomTabs.js +37 -0
- package/js/components/connectionCard.js +106 -0
- package/js/components/dataGrid.js +47 -0
- package/js/components/emptyState.js +159 -0
- package/js/components/metricCard.js +32 -0
- package/js/components/modal.js +317 -0
- package/js/components/pageHeader.js +33 -0
- package/js/components/queryEditor.js +121 -0
- package/js/components/queryResults.js +107 -0
- package/js/components/rowEditorPanel.js +164 -0
- package/js/components/sidebar.js +57 -0
- package/js/components/statusBar.js +39 -0
- package/js/components/toast.js +39 -0
- package/js/components/topNav.js +27 -0
- package/js/router.js +66 -0
- package/js/store.js +1092 -0
- package/js/utils/format.js +179 -0
- package/js/views/connections.js +133 -0
- package/js/views/data.js +400 -0
- package/js/views/editor.js +259 -0
- package/js/views/landing.js +11 -0
- package/js/views/overview.js +220 -0
- package/js/views/settings.js +109 -0
- package/js/views/structure.js +242 -0
- package/package.json +18 -0
- package/publish_brew.sh +444 -0
- package/publish_npm.sh +241 -0
- package/server/routes/connections.js +146 -0
- package/server/routes/data.js +59 -0
- package/server/routes/export.js +25 -0
- package/server/routes/overview.js +39 -0
- package/server/routes/settings.js +50 -0
- package/server/routes/sql.js +50 -0
- package/server/routes/structure.js +38 -0
- package/server/server.js +136 -0
- package/server/services/sqlite/connectionManager.js +306 -0
- package/server/services/sqlite/dataBrowserService.js +255 -0
- package/server/services/sqlite/exportService.js +34 -0
- package/server/services/sqlite/importService.js +111 -0
- package/server/services/sqlite/introspection.js +302 -0
- package/server/services/sqlite/overviewService.js +109 -0
- package/server/services/sqlite/sqlExecutor.js +434 -0
- package/server/services/sqlite/structureService.js +60 -0
- package/server/services/storage/appStateStore.js +530 -0
- package/server/utils/csv.js +34 -0
- package/server/utils/errors.js +175 -0
- package/server/utils/fileValidation.js +135 -0
- package/server/utils/identifier.js +38 -0
- package/server/utils/sqliteTypes.js +112 -0
- package/styles/base.css +176 -0
- package/styles/components.css +323 -0
- package/styles/layout.css +101 -0
- package/styles/tokens.css +49 -0
- package/styles/views.css +84 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { escapeHtml, highlightSql } from "../utils/format.js";
|
|
2
|
+
import { renderActionBar } from "./actionBar.js";
|
|
3
|
+
|
|
4
|
+
function renderLineNumbers(query) {
|
|
5
|
+
const lineCount = Math.max(1, String(query || "").split("\n").length);
|
|
6
|
+
|
|
7
|
+
return Array.from({ length: lineCount }, (_, index) => index + 1)
|
|
8
|
+
.map((value) => `<span>${String(value).padStart(2, "0")}</span>`)
|
|
9
|
+
.join("");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function renderHistoryOptions(history) {
|
|
13
|
+
if (!history.length) {
|
|
14
|
+
return '<option value="">No recent statements</option>';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return [
|
|
18
|
+
'<option value="">Load recent statement...</option>',
|
|
19
|
+
...history.map(
|
|
20
|
+
(entry) => `
|
|
21
|
+
<option value="${escapeHtml(entry.id)}">
|
|
22
|
+
${escapeHtml(entry.sql.replace(/\s+/g, " ").slice(0, 96))}
|
|
23
|
+
</option>
|
|
24
|
+
`
|
|
25
|
+
),
|
|
26
|
+
].join("");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function renderHighlightedQuery(query) {
|
|
30
|
+
if (query) {
|
|
31
|
+
return highlightSql(query);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return '<span class="text-on-surface-variant/35">SELECT name FROM sqlite_master WHERE type = \'table\';</span>';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function renderQueryEditor({
|
|
38
|
+
query,
|
|
39
|
+
title,
|
|
40
|
+
executing = false,
|
|
41
|
+
history = [],
|
|
42
|
+
historyLoading = false,
|
|
43
|
+
}) {
|
|
44
|
+
const left = `
|
|
45
|
+
<div class="flex items-center gap-2 bg-surface-container-lowest px-3 py-1">
|
|
46
|
+
<span class="material-symbols-outlined text-xs text-[#FCE300]">database</span>
|
|
47
|
+
<span class="text-[10px] font-mono uppercase tracking-widest text-on-surface-variant">${escapeHtml(
|
|
48
|
+
title
|
|
49
|
+
)}</span>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="hidden items-center gap-2 text-[10px] font-mono uppercase tracking-widest text-on-surface-variant/40 md:flex">
|
|
52
|
+
<span class="material-symbols-outlined text-xs">history</span>
|
|
53
|
+
${historyLoading ? "Loading history..." : `${history.length} statements in history`}
|
|
54
|
+
</div>
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
const right = `
|
|
58
|
+
<select
|
|
59
|
+
class="min-w-[220px] border border-outline-variant/20 bg-surface-container-lowest px-3 py-2 text-[10px] font-mono uppercase tracking-[0.14em] text-on-surface-variant outline-none"
|
|
60
|
+
data-bind="history-entry"
|
|
61
|
+
>
|
|
62
|
+
${renderHistoryOptions(history)}
|
|
63
|
+
</select>
|
|
64
|
+
<button
|
|
65
|
+
class="px-4 py-1.5 text-[10px] font-bold uppercase tracking-widest text-on-surface hover:bg-surface-container-highest transition-colors"
|
|
66
|
+
data-action="clear-sql-history"
|
|
67
|
+
type="button"
|
|
68
|
+
>
|
|
69
|
+
Clear History
|
|
70
|
+
</button>
|
|
71
|
+
<button
|
|
72
|
+
class="px-4 py-1.5 text-[10px] font-bold uppercase tracking-widest text-on-surface hover:bg-surface-container-highest transition-colors"
|
|
73
|
+
data-action="clear-query"
|
|
74
|
+
type="button"
|
|
75
|
+
>
|
|
76
|
+
Clear
|
|
77
|
+
</button>
|
|
78
|
+
<button
|
|
79
|
+
class="bg-primary-container px-6 py-1.5 text-xs font-black uppercase tracking-tighter text-on-primary shadow-[0_0_15px_-5px_rgba(252,227,0,0.4)] transition-all hover:brightness-110"
|
|
80
|
+
data-action="execute-query"
|
|
81
|
+
type="button"
|
|
82
|
+
>
|
|
83
|
+
${executing ? "RUNNING..." : "EXECUTE"}
|
|
84
|
+
</button>
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
return `
|
|
88
|
+
<div class="flex h-full flex-col">
|
|
89
|
+
<div class="bg-surface-container-low px-6 py-3">
|
|
90
|
+
${renderActionBar({
|
|
91
|
+
left,
|
|
92
|
+
right,
|
|
93
|
+
className: "flex-wrap",
|
|
94
|
+
})}
|
|
95
|
+
</div>
|
|
96
|
+
<div class="flex flex-1 overflow-hidden">
|
|
97
|
+
<div class="flex w-12 flex-col items-center bg-surface-container-lowest py-4 font-mono text-xs select-none text-outline-variant/30">
|
|
98
|
+
${renderLineNumbers(query)}
|
|
99
|
+
</div>
|
|
100
|
+
<div class="relative flex-1 overflow-hidden bg-surface-container p-6 font-mono text-sm leading-relaxed">
|
|
101
|
+
<div class="pointer-events-none absolute right-0 top-0 p-4 opacity-5">
|
|
102
|
+
<span class="material-symbols-outlined text-[120px] font-thin">terminal</span>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="query-editor-layer relative z-10 h-full min-h-[140px]">
|
|
105
|
+
<pre
|
|
106
|
+
aria-hidden="true"
|
|
107
|
+
class="query-editor-highlight"
|
|
108
|
+
data-query-editor-highlight
|
|
109
|
+
>${renderHighlightedQuery(query)}</pre>
|
|
110
|
+
<textarea
|
|
111
|
+
class="query-editor-input custom-scrollbar relative z-10 h-full min-h-[140px] w-full resize-none border-none focus:ring-0"
|
|
112
|
+
data-bind="current-query"
|
|
113
|
+
placeholder="SELECT name FROM sqlite_master WHERE type = 'table';"
|
|
114
|
+
spellcheck="false"
|
|
115
|
+
>${escapeHtml(query)}</textarea>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { renderDataGrid } from "./dataGrid.js";
|
|
2
|
+
import { escapeHtml, formatCellValue, formatNumber } from "../utils/format.js";
|
|
3
|
+
|
|
4
|
+
export function renderQueryResultsPane(
|
|
5
|
+
result,
|
|
6
|
+
{ exporting = false, selectedRowIndex = null, editable = false, editStatusMessage = "" } = {}
|
|
7
|
+
) {
|
|
8
|
+
if (!result) {
|
|
9
|
+
return `
|
|
10
|
+
<div class="flex h-full flex-col items-center justify-center bg-surface-container-lowest text-on-surface-variant/30">
|
|
11
|
+
<span class="material-symbols-outlined mb-3 text-5xl">database_off</span>
|
|
12
|
+
<p class="font-mono text-[10px] uppercase tracking-[0.22em]">
|
|
13
|
+
NO_QUERY_RESULTS_AVAILABLE
|
|
14
|
+
</p>
|
|
15
|
+
</div>
|
|
16
|
+
`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const columns = (result.columns ?? []).map((columnName) => ({
|
|
20
|
+
label: escapeHtml(columnName),
|
|
21
|
+
headerClassName:
|
|
22
|
+
"border-b-2 border-primary-container px-4 py-3 text-[10px] font-bold uppercase tracking-widest",
|
|
23
|
+
cellClassName: "px-4 py-3 align-top text-on-surface",
|
|
24
|
+
render: (row) => {
|
|
25
|
+
const value = formatCellValue(row[columnName]);
|
|
26
|
+
const isNull = value === "NULL";
|
|
27
|
+
return `<span class="${
|
|
28
|
+
isNull ? "text-on-surface-variant/40" : "text-on-surface"
|
|
29
|
+
}">${escapeHtml(value)}</span>`;
|
|
30
|
+
},
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
return `
|
|
34
|
+
<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
|
+
<div class="custom-scrollbar min-h-0 flex-1 overflow-auto bg-surface-container-lowest">
|
|
80
|
+
${
|
|
81
|
+
result.columns?.length
|
|
82
|
+
? renderDataGrid({
|
|
83
|
+
columns,
|
|
84
|
+
rows: result.rows ?? [],
|
|
85
|
+
tableClass: "min-w-full border-collapse text-left font-mono text-xs",
|
|
86
|
+
theadClass: "sticky top-0 z-10 bg-surface-container-highest text-on-surface",
|
|
87
|
+
tbodyClass: "divide-y divide-outline-variant/5",
|
|
88
|
+
getRowClass: (_, index) =>
|
|
89
|
+
`${selectedRowIndex === index ? "bg-surface-bright" : index % 2 === 0 ? "bg-surface-container-low" : "bg-surface-container-lowest"} transition-colors ${
|
|
90
|
+
editable ? "cursor-pointer hover:bg-surface-bright" : "hover:bg-surface-bright"
|
|
91
|
+
}`,
|
|
92
|
+
getRowAttrs: (_, index) =>
|
|
93
|
+
editable ? `data-action="select-editor-row" data-row-index="${index}"` : "",
|
|
94
|
+
})
|
|
95
|
+
: `
|
|
96
|
+
<div class="flex h-full flex-col items-center justify-center text-center text-on-surface-variant/35">
|
|
97
|
+
<span class="material-symbols-outlined mb-3 text-4xl">rule</span>
|
|
98
|
+
<p class="font-mono text-[10px] uppercase tracking-[0.22em]">
|
|
99
|
+
STATEMENT_RETURNED_NO_RESULT_SET
|
|
100
|
+
</p>
|
|
101
|
+
</div>
|
|
102
|
+
`
|
|
103
|
+
}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { escapeHtml } from "../utils/format.js";
|
|
2
|
+
|
|
3
|
+
function renderReadonlyField(label, value) {
|
|
4
|
+
return `
|
|
5
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
|
|
6
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
7
|
+
${escapeHtml(label)}
|
|
8
|
+
</div>
|
|
9
|
+
<div class="mt-2 text-sm text-on-surface">${escapeHtml(value)}</div>
|
|
10
|
+
</div>
|
|
11
|
+
`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function renderRowEditorPanel({
|
|
15
|
+
title,
|
|
16
|
+
sectionLabel = "Row Editor",
|
|
17
|
+
subtitle = "",
|
|
18
|
+
closeAction,
|
|
19
|
+
formName,
|
|
20
|
+
hiddenFields = [],
|
|
21
|
+
editableFields = [],
|
|
22
|
+
readonlyFields = [],
|
|
23
|
+
disabledMessage = "",
|
|
24
|
+
saveError = null,
|
|
25
|
+
saving = false,
|
|
26
|
+
reloadAction = "",
|
|
27
|
+
submitLabel = "Save Row",
|
|
28
|
+
emptyEditableMessage = "This row has no editable scalar columns.",
|
|
29
|
+
}) {
|
|
30
|
+
const canSubmit = !disabledMessage && editableFields.length > 0;
|
|
31
|
+
|
|
32
|
+
return `
|
|
33
|
+
<section class="flex h-full min-h-0 flex-col bg-surface-low">
|
|
34
|
+
<header class="border-b border-outline-variant/10 bg-surface-container px-6 py-5">
|
|
35
|
+
<div class="flex items-start justify-between gap-4">
|
|
36
|
+
<div>
|
|
37
|
+
<div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
|
|
38
|
+
${escapeHtml(sectionLabel)}
|
|
39
|
+
</div>
|
|
40
|
+
<h2 class="mt-2 font-headline text-3xl font-black uppercase tracking-tight text-primary-container">
|
|
41
|
+
${escapeHtml(title)}
|
|
42
|
+
</h2>
|
|
43
|
+
${
|
|
44
|
+
subtitle
|
|
45
|
+
? `
|
|
46
|
+
<div class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
47
|
+
${escapeHtml(subtitle)}
|
|
48
|
+
</div>
|
|
49
|
+
`
|
|
50
|
+
: ""
|
|
51
|
+
}
|
|
52
|
+
</div>
|
|
53
|
+
<button
|
|
54
|
+
class="border border-outline-variant/20 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface hover:bg-surface-container-highest"
|
|
55
|
+
data-action="${escapeHtml(closeAction)}"
|
|
56
|
+
type="button"
|
|
57
|
+
>
|
|
58
|
+
Close
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
</header>
|
|
62
|
+
<div class="custom-scrollbar flex-1 overflow-auto px-6 py-6">
|
|
63
|
+
${
|
|
64
|
+
disabledMessage
|
|
65
|
+
? `
|
|
66
|
+
<div class="border border-error/20 bg-error-container/10 px-4 py-4 text-sm text-on-surface">
|
|
67
|
+
${escapeHtml(disabledMessage)}
|
|
68
|
+
</div>
|
|
69
|
+
`
|
|
70
|
+
: `
|
|
71
|
+
<form class="space-y-6" data-form="${escapeHtml(formName)}">
|
|
72
|
+
${hiddenFields
|
|
73
|
+
.map(
|
|
74
|
+
(field) => `
|
|
75
|
+
<input
|
|
76
|
+
name="${escapeHtml(field.name)}"
|
|
77
|
+
type="hidden"
|
|
78
|
+
value="${escapeHtml(field.value ?? "")}"
|
|
79
|
+
/>
|
|
80
|
+
`
|
|
81
|
+
)
|
|
82
|
+
.join("")}
|
|
83
|
+
${
|
|
84
|
+
editableFields.length
|
|
85
|
+
? `
|
|
86
|
+
<div class="space-y-4">
|
|
87
|
+
${editableFields
|
|
88
|
+
.map(
|
|
89
|
+
(field) => `
|
|
90
|
+
<label class="block space-y-2">
|
|
91
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
92
|
+
${escapeHtml(field.label ?? field.name)}
|
|
93
|
+
</span>
|
|
94
|
+
<textarea
|
|
95
|
+
class="min-h-[112px] w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
96
|
+
name="field:${escapeHtml(field.name)}"
|
|
97
|
+
>${escapeHtml(field.value ?? "")}</textarea>
|
|
98
|
+
</label>
|
|
99
|
+
`
|
|
100
|
+
)
|
|
101
|
+
.join("")}
|
|
102
|
+
</div>
|
|
103
|
+
`
|
|
104
|
+
: `<div class="text-sm text-on-surface-variant/55">${escapeHtml(
|
|
105
|
+
emptyEditableMessage
|
|
106
|
+
)}</div>`
|
|
107
|
+
}
|
|
108
|
+
${
|
|
109
|
+
saveError
|
|
110
|
+
? `
|
|
111
|
+
<div class="border border-error/20 bg-error-container/10 px-4 py-4 text-sm text-on-surface">
|
|
112
|
+
<div class="font-headline text-xs font-bold uppercase tracking-[0.18em] text-error">
|
|
113
|
+
${escapeHtml(saveError.code)}
|
|
114
|
+
</div>
|
|
115
|
+
<div class="mt-2">${escapeHtml(saveError.message)}</div>
|
|
116
|
+
</div>
|
|
117
|
+
`
|
|
118
|
+
: ""
|
|
119
|
+
}
|
|
120
|
+
<div class="flex items-center justify-end gap-3 border-t border-outline-variant/10 pt-6">
|
|
121
|
+
${
|
|
122
|
+
reloadAction
|
|
123
|
+
? `
|
|
124
|
+
<button
|
|
125
|
+
class="border border-outline-variant/20 px-4 py-3 text-xs font-bold uppercase tracking-[0.18em] text-on-surface hover:bg-surface-container-highest"
|
|
126
|
+
data-action="${escapeHtml(reloadAction)}"
|
|
127
|
+
type="button"
|
|
128
|
+
>
|
|
129
|
+
Reload
|
|
130
|
+
</button>
|
|
131
|
+
`
|
|
132
|
+
: ""
|
|
133
|
+
}
|
|
134
|
+
<button
|
|
135
|
+
class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary disabled:cursor-default disabled:opacity-40"
|
|
136
|
+
type="submit"
|
|
137
|
+
${canSubmit ? "" : "disabled"}
|
|
138
|
+
>
|
|
139
|
+
${escapeHtml(saving ? "Saving..." : submitLabel)}
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
142
|
+
</form>
|
|
143
|
+
`
|
|
144
|
+
}
|
|
145
|
+
${
|
|
146
|
+
readonlyFields.length
|
|
147
|
+
? `
|
|
148
|
+
<div class="mt-8 space-y-3">
|
|
149
|
+
<div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
|
|
150
|
+
Locked Fields
|
|
151
|
+
</div>
|
|
152
|
+
<div class="space-y-3">
|
|
153
|
+
${readonlyFields
|
|
154
|
+
.map((field) => renderReadonlyField(field.label ?? field.name, field.value))
|
|
155
|
+
.join("")}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
`
|
|
159
|
+
: ""
|
|
160
|
+
}
|
|
161
|
+
</div>
|
|
162
|
+
</section>
|
|
163
|
+
`;
|
|
164
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { escapeHtml } from "../utils/format.js";
|
|
2
|
+
|
|
3
|
+
const sidebarItems = [
|
|
4
|
+
{ label: "Connections", href: "#/connections", key: "connections", icon: "database" },
|
|
5
|
+
{ label: "Overview", href: "#/overview", key: "overview", icon: "dashboard" },
|
|
6
|
+
{ label: "Data", href: "#/data", key: "data", icon: "table_rows" },
|
|
7
|
+
{ label: "SQL Editor", href: "#/editor", key: "editor", icon: "terminal" },
|
|
8
|
+
{ label: "Structure", href: "#/structure", key: "structure", icon: "account_tree" },
|
|
9
|
+
{ label: "Settings", href: "#/settings", key: "settings", icon: "settings" },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function getActiveSidebarKey(routeName) {
|
|
13
|
+
if (routeName === "landing") {
|
|
14
|
+
return "connections";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (routeName === "editorResults") {
|
|
18
|
+
return "editor";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return routeName;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function renderSidebar(state) {
|
|
25
|
+
const activeKey = getActiveSidebarKey(state.route.name);
|
|
26
|
+
const activeConnection = state.connections.active;
|
|
27
|
+
|
|
28
|
+
return `
|
|
29
|
+
<nav class="sidebar-links">
|
|
30
|
+
${sidebarItems
|
|
31
|
+
.map(
|
|
32
|
+
(item) => `
|
|
33
|
+
<a class="sidebar-link ${item.key === activeKey ? "is-active" : ""}" href="${item.href}">
|
|
34
|
+
<span class="material-symbols-outlined">${item.icon}</span>
|
|
35
|
+
<span>${item.label}</span>
|
|
36
|
+
</a>
|
|
37
|
+
`
|
|
38
|
+
)
|
|
39
|
+
.join("")}
|
|
40
|
+
</nav>
|
|
41
|
+
<div class="sidebar-footer">
|
|
42
|
+
<div class="sidebar-footer-card">
|
|
43
|
+
<div class="sidebar-footer-mark">
|
|
44
|
+
<span class="material-symbols-outlined text-[15px]">memory</span>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="min-w-0">
|
|
47
|
+
<p class="truncate text-[10px] font-bold text-on-surface">
|
|
48
|
+
${escapeHtml(activeConnection?.label ?? "NO_ACTIVE_DATABASE")}
|
|
49
|
+
</p>
|
|
50
|
+
<p class="text-[8px] text-on-surface-variant/60">
|
|
51
|
+
${activeConnection?.readOnly ? "READ_ONLY" : "READ_WRITE"}
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { escapeHtml, formatBytes, formatDateTime } from "../utils/format.js";
|
|
2
|
+
|
|
3
|
+
export function renderStatusBar(state) {
|
|
4
|
+
const active = state.connections.active;
|
|
5
|
+
const overview = state.overview.data;
|
|
6
|
+
const result = state.editor.result;
|
|
7
|
+
|
|
8
|
+
return `
|
|
9
|
+
<div class="status-bar-shell">
|
|
10
|
+
<div class="status-bar-primary">
|
|
11
|
+
<span class="status-bar-text">
|
|
12
|
+
${
|
|
13
|
+
active
|
|
14
|
+
? `ACTIVE_DB // ${escapeHtml(active.label)}`
|
|
15
|
+
: "NO_ACTIVE_DATABASE // CONNECT_SQLITE_FILE"
|
|
16
|
+
}
|
|
17
|
+
</span>
|
|
18
|
+
<div class="status-bar-dot"></div>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="status-bar-secondary">
|
|
21
|
+
<span class="status-bar-link">
|
|
22
|
+
${active ? (active.readOnly ? "READ_ONLY" : "READ_WRITE") : "IDLE"}
|
|
23
|
+
</span>
|
|
24
|
+
<span class="status-bar-link">
|
|
25
|
+
${overview?.sqlite?.journalMode ? escapeHtml(overview.sqlite.journalMode) : "journal:n/a"}
|
|
26
|
+
</span>
|
|
27
|
+
<span class="status-bar-link">
|
|
28
|
+
${overview?.file?.sizeBytes ? escapeHtml(formatBytes(overview.file.sizeBytes)) : "size:n/a"}
|
|
29
|
+
</span>
|
|
30
|
+
<span class="status-bar-link">
|
|
31
|
+
${result ? `last query ${escapeHtml(String(result.timingMs ?? 0))}ms` : "no query executed"}
|
|
32
|
+
</span>
|
|
33
|
+
<span class="status-bar-link">
|
|
34
|
+
${active?.lastOpenedAt ? escapeHtml(formatDateTime(active.lastOpenedAt)) : "waiting"}
|
|
35
|
+
</span>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { escapeHtml } from "../utils/format.js";
|
|
2
|
+
|
|
3
|
+
export function renderToasts(toasts = []) {
|
|
4
|
+
if (!toasts.length) {
|
|
5
|
+
return "";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return `
|
|
9
|
+
<div class="pointer-events-none fixed bottom-5 right-5 z-50 flex w-full max-w-sm flex-col gap-3">
|
|
10
|
+
${toasts
|
|
11
|
+
.map(
|
|
12
|
+
(toast) => `
|
|
13
|
+
<div
|
|
14
|
+
class="pointer-events-auto border border-outline-variant/20 bg-surface-container px-4 py-3 shadow-[0_18px_40px_rgba(0,0,0,0.35)] ${
|
|
15
|
+
toast.tone === "success"
|
|
16
|
+
? "border-primary-container/30"
|
|
17
|
+
: toast.tone === "alert"
|
|
18
|
+
? "border-error/30"
|
|
19
|
+
: ""
|
|
20
|
+
}"
|
|
21
|
+
>
|
|
22
|
+
<div class="flex items-start justify-between gap-3">
|
|
23
|
+
<div class="text-sm text-on-surface">${escapeHtml(toast.message)}</div>
|
|
24
|
+
<button
|
|
25
|
+
class="text-on-surface-variant hover:text-primary-container"
|
|
26
|
+
data-action="dismiss-toast"
|
|
27
|
+
data-toast-id="${escapeHtml(toast.id)}"
|
|
28
|
+
type="button"
|
|
29
|
+
>
|
|
30
|
+
<span class="material-symbols-outlined text-base">close</span>
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
`
|
|
35
|
+
)
|
|
36
|
+
.join("")}
|
|
37
|
+
</div>
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function renderTopNav() {
|
|
2
|
+
return `
|
|
3
|
+
<div class="top-nav-shell">
|
|
4
|
+
<a class="top-nav-brand" href="#/">SQLite Hub</a>
|
|
5
|
+
<div class="top-nav-actions">
|
|
6
|
+
<button class="top-nav-icon" data-action="refresh-view" type="button" aria-label="Refresh">
|
|
7
|
+
<span class="material-symbols-outlined">refresh</span>
|
|
8
|
+
</button>
|
|
9
|
+
<button
|
|
10
|
+
class="top-nav-icon"
|
|
11
|
+
data-action="open-modal"
|
|
12
|
+
data-modal="open-connection"
|
|
13
|
+
type="button"
|
|
14
|
+
aria-label="Open Database"
|
|
15
|
+
>
|
|
16
|
+
<span class="material-symbols-outlined">folder_open</span>
|
|
17
|
+
</button>
|
|
18
|
+
<button class="top-nav-icon" data-action="navigate" data-to="/editor" type="button" aria-label="SQL Editor">
|
|
19
|
+
<span class="material-symbols-outlined">terminal</span>
|
|
20
|
+
</button>
|
|
21
|
+
<button class="top-nav-icon" data-action="navigate" data-to="/settings" type="button" aria-label="Settings">
|
|
22
|
+
<span class="material-symbols-outlined">settings</span>
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
`;
|
|
27
|
+
}
|
package/js/router.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export function parseHash(hash = window.location.hash) {
|
|
2
|
+
const normalized = hash.startsWith("#") ? hash.slice(1) : hash;
|
|
3
|
+
const [pathname = "/"] = normalized.split("?");
|
|
4
|
+
const cleanPath = pathname || "/";
|
|
5
|
+
const segments = cleanPath.split("/").filter(Boolean);
|
|
6
|
+
|
|
7
|
+
if (segments.length === 0) {
|
|
8
|
+
return { name: "landing", path: "/", params: {} };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
switch (segments[0]) {
|
|
12
|
+
case "connections":
|
|
13
|
+
return { name: "connections", path: "/connections", params: {} };
|
|
14
|
+
case "overview":
|
|
15
|
+
return { name: "overview", path: "/overview", params: {} };
|
|
16
|
+
case "editor":
|
|
17
|
+
if (segments[1] === "results") {
|
|
18
|
+
return { name: "editorResults", path: "/editor/results", params: {} };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { name: "editor", path: "/editor", params: {} };
|
|
22
|
+
case "data":
|
|
23
|
+
return {
|
|
24
|
+
name: "data",
|
|
25
|
+
path: cleanPath,
|
|
26
|
+
params: {
|
|
27
|
+
tableName: segments[1] ? decodeURIComponent(segments[1]) : null,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
case "structure":
|
|
31
|
+
return { name: "structure", path: "/structure", params: {} };
|
|
32
|
+
case "settings":
|
|
33
|
+
return { name: "settings", path: "/settings", params: {} };
|
|
34
|
+
default:
|
|
35
|
+
return { name: "notFound", path: cleanPath, params: {} };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createRouter(onRouteChange) {
|
|
40
|
+
const handleRouteChange = () => {
|
|
41
|
+
onRouteChange(parseHash(window.location.hash));
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
start() {
|
|
46
|
+
window.addEventListener("hashchange", handleRouteChange);
|
|
47
|
+
|
|
48
|
+
if (!window.location.hash) {
|
|
49
|
+
window.location.hash = "#/";
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
handleRouteChange();
|
|
54
|
+
},
|
|
55
|
+
navigate(path) {
|
|
56
|
+
const nextHash = path.startsWith("#") ? path : `#${path}`;
|
|
57
|
+
|
|
58
|
+
if (window.location.hash === nextHash) {
|
|
59
|
+
handleRouteChange();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
window.location.hash = nextHash;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|