sqlite-hub 0.2.0 → 0.3.1
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/.github/funding.yml +2 -0
- package/LICENSE +21 -0
- package/README.md +53 -27
- package/assets/mockups/graph_visualize.png +0 -0
- package/changelog.md +7 -1
- package/index.html +3 -0
- package/js/api.js +14 -0
- package/js/app.js +42 -1
- package/js/components/queryEditor.js +8 -0
- package/js/components/structureGraph.js +925 -0
- package/js/lib/cytoscapeRuntime.js +16 -0
- package/js/store.js +55 -0
- package/js/views/connections.js +18 -3
- package/js/views/data.js +13 -0
- package/js/views/editor.js +1 -0
- package/js/views/structure.js +239 -117
- package/package.json +4 -1
- package/publish_brew.sh +22 -0
- package/server/routes/connections.js +15 -1
- package/server/routes/export.js +26 -6
- package/server/server.js +16 -0
- package/server/services/sqlite/backupService.js +112 -0
- package/server/services/sqlite/connectionManager.js +15 -0
- package/server/services/sqlite/exportService.js +46 -1
- package/server/services/sqlite/structureService.js +26 -0
- package/server/services/storage/appStateStore.js +7 -2
- package/styles/structure-graph.css +414 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import cytoscape from "/vendor/cytoscape/dist/cytoscape.esm.min.mjs";
|
|
2
|
+
|
|
3
|
+
let elkRegistered = false;
|
|
4
|
+
|
|
5
|
+
export function getCytoscape() {
|
|
6
|
+
if (!elkRegistered) {
|
|
7
|
+
if (typeof window.cytoscapeElk !== "function") {
|
|
8
|
+
throw new Error("cytoscape-elk is not available on window.");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
cytoscape.use(window.cytoscapeElk);
|
|
12
|
+
elkRegistered = true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return cytoscape;
|
|
16
|
+
}
|
package/js/store.js
CHANGED
|
@@ -24,6 +24,7 @@ const state = {
|
|
|
24
24
|
recent: [],
|
|
25
25
|
active: null,
|
|
26
26
|
loading: false,
|
|
27
|
+
backupLoading: false,
|
|
27
28
|
error: null,
|
|
28
29
|
},
|
|
29
30
|
settings: {
|
|
@@ -47,6 +48,7 @@ const state = {
|
|
|
47
48
|
page: 1,
|
|
48
49
|
pageSize: 50,
|
|
49
50
|
selectedRowIndex: null,
|
|
51
|
+
exportLoading: false,
|
|
50
52
|
error: null,
|
|
51
53
|
saveError: null,
|
|
52
54
|
},
|
|
@@ -159,6 +161,7 @@ function setMissingDatabaseState() {
|
|
|
159
161
|
state.dataBrowser.table = null;
|
|
160
162
|
state.dataBrowser.page = 1;
|
|
161
163
|
state.dataBrowser.selectedRowIndex = null;
|
|
164
|
+
state.dataBrowser.exportLoading = false;
|
|
162
165
|
state.dataBrowser.error = error;
|
|
163
166
|
state.dataBrowser.saveError = null;
|
|
164
167
|
|
|
@@ -480,6 +483,7 @@ function invalidateDatabaseCaches() {
|
|
|
480
483
|
state.dataBrowser.table = null;
|
|
481
484
|
state.dataBrowser.page = 1;
|
|
482
485
|
state.dataBrowser.selectedRowIndex = null;
|
|
486
|
+
state.dataBrowser.exportLoading = false;
|
|
483
487
|
state.dataBrowser.error = null;
|
|
484
488
|
state.dataBrowser.saveError = null;
|
|
485
489
|
state.structure.data = null;
|
|
@@ -743,6 +747,32 @@ export async function removeConnection(id) {
|
|
|
743
747
|
}
|
|
744
748
|
}
|
|
745
749
|
|
|
750
|
+
export async function createActiveConnectionBackup() {
|
|
751
|
+
if (!state.connections.active) {
|
|
752
|
+
pushToast("No active SQLite database selected for backup.", "alert");
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
state.connections.backupLoading = true;
|
|
757
|
+
emitChange();
|
|
758
|
+
|
|
759
|
+
try {
|
|
760
|
+
const response = await api.createActiveConnectionBackup();
|
|
761
|
+
await refreshConnectionsState();
|
|
762
|
+
pushToast(response.message || "Backup created.", "success");
|
|
763
|
+
return response.data;
|
|
764
|
+
} catch (error) {
|
|
765
|
+
pushToast(
|
|
766
|
+
normalizeError(error)?.message || "Backup could not be created.",
|
|
767
|
+
"alert"
|
|
768
|
+
);
|
|
769
|
+
return null;
|
|
770
|
+
} finally {
|
|
771
|
+
state.connections.backupLoading = false;
|
|
772
|
+
emitChange();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
746
776
|
export function setCurrentQuery(query) {
|
|
747
777
|
const nextQuery = String(query ?? "");
|
|
748
778
|
const previousLineCount = Math.max(1, String(state.editor.sqlText || "").split("\n").length);
|
|
@@ -1027,6 +1057,31 @@ export async function exportCurrentQueryCsv() {
|
|
|
1027
1057
|
}
|
|
1028
1058
|
}
|
|
1029
1059
|
|
|
1060
|
+
export async function exportCurrentDataTableCsv() {
|
|
1061
|
+
const tableName = state.dataBrowser.selectedTable;
|
|
1062
|
+
|
|
1063
|
+
if (!tableName) {
|
|
1064
|
+
pushToast("No table selected for export.", "alert");
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
state.dataBrowser.exportLoading = true;
|
|
1069
|
+
emitChange();
|
|
1070
|
+
|
|
1071
|
+
try {
|
|
1072
|
+
await api.downloadTableCsv(tableName);
|
|
1073
|
+
pushToast(`CSV export started for ${tableName}.`, "success");
|
|
1074
|
+
return true;
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
state.dataBrowser.error = normalizeError(error);
|
|
1077
|
+
emitChange();
|
|
1078
|
+
return false;
|
|
1079
|
+
} finally {
|
|
1080
|
+
state.dataBrowser.exportLoading = false;
|
|
1081
|
+
emitChange();
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1030
1085
|
export async function refreshCurrentRoute() {
|
|
1031
1086
|
await loadRouteData(state.route);
|
|
1032
1087
|
}
|
package/js/views/connections.js
CHANGED
|
@@ -5,9 +5,11 @@ import { escapeHtml } from "../utils/format.js";
|
|
|
5
5
|
function renderConnectionsActionButton({
|
|
6
6
|
label,
|
|
7
7
|
icon,
|
|
8
|
+
action = "open-modal",
|
|
8
9
|
modal,
|
|
9
10
|
tone = "secondary",
|
|
10
11
|
className = "",
|
|
12
|
+
disabled = false,
|
|
11
13
|
}) {
|
|
12
14
|
const clipPath = "polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%)";
|
|
13
15
|
const toneClassName =
|
|
@@ -19,14 +21,16 @@ function renderConnectionsActionButton({
|
|
|
19
21
|
? "text-base text-on-primary"
|
|
20
22
|
: "text-base text-primary-container/90";
|
|
21
23
|
const clipStyle = `style="--clip-path: ${clipPath};"`;
|
|
24
|
+
const modalAttribute = modal ? `data-modal="${modal}"` : "";
|
|
22
25
|
|
|
23
26
|
return `
|
|
24
27
|
<button
|
|
25
|
-
class="flex h-11 items-center justify-between gap-6 px-5 font-headline text-xs font-bold uppercase tracking-[0.18em] transition-colors ${toneClassName} ${className}"
|
|
26
|
-
data-action="
|
|
27
|
-
|
|
28
|
+
class="flex h-11 items-center justify-between gap-6 px-5 font-headline text-xs font-bold uppercase tracking-[0.18em] transition-colors disabled:cursor-default disabled:opacity-40 ${toneClassName} ${className}"
|
|
29
|
+
data-action="${escapeHtml(action)}"
|
|
30
|
+
${modalAttribute}
|
|
28
31
|
${clipStyle}
|
|
29
32
|
type="button"
|
|
33
|
+
${disabled ? "disabled" : ""}
|
|
30
34
|
>
|
|
31
35
|
<span>${label}</span>
|
|
32
36
|
<span class="material-symbols-outlined ${iconClassName}">${icon}</span>
|
|
@@ -112,6 +116,17 @@ export function renderConnectionsView(state) {
|
|
|
112
116
|
modal: "create-connection",
|
|
113
117
|
className: "min-w-[13rem]",
|
|
114
118
|
})}
|
|
119
|
+
${
|
|
120
|
+
state.connections.active
|
|
121
|
+
? renderConnectionsActionButton({
|
|
122
|
+
label: state.connections.backupLoading ? "Creating Backup..." : "Create Backup",
|
|
123
|
+
icon: "inventory_2",
|
|
124
|
+
action: "create-backup",
|
|
125
|
+
className: "min-w-[13rem]",
|
|
126
|
+
disabled: state.connections.backupLoading,
|
|
127
|
+
})
|
|
128
|
+
: ""
|
|
129
|
+
}
|
|
115
130
|
`;
|
|
116
131
|
|
|
117
132
|
return {
|
package/js/views/data.js
CHANGED
|
@@ -95,6 +95,19 @@ function renderWorkspaceHeader(state) {
|
|
|
95
95
|
</div>
|
|
96
96
|
</div>
|
|
97
97
|
<div class="flex items-center gap-3">
|
|
98
|
+
${
|
|
99
|
+
table
|
|
100
|
+
? `
|
|
101
|
+
<button
|
|
102
|
+
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"
|
|
103
|
+
data-action="export-data-csv"
|
|
104
|
+
type="button"
|
|
105
|
+
>
|
|
106
|
+
${state.dataBrowser.exportLoading ? "Exporting..." : "Export CSV"}
|
|
107
|
+
</button>
|
|
108
|
+
`
|
|
109
|
+
: ""
|
|
110
|
+
}
|
|
98
111
|
<button
|
|
99
112
|
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"
|
|
100
113
|
data-action="refresh-view"
|
package/js/views/editor.js
CHANGED
|
@@ -243,6 +243,7 @@ export function renderEditorView(state, { isResultsRoute = false } = {}) {
|
|
|
243
243
|
${renderQueryEditor({
|
|
244
244
|
query: state.editor.sqlText,
|
|
245
245
|
executing: state.editor.executing,
|
|
246
|
+
exporting: state.editor.exportLoading,
|
|
246
247
|
history: state.editor.history,
|
|
247
248
|
historyLoading: state.editor.historyLoading,
|
|
248
249
|
title: connection?.label ?? "SQLite Query Workspace",
|
package/js/views/structure.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { renderMetricCard } from "../components/metricCard.js";
|
|
2
1
|
import { renderPageHeader } from "../components/pageHeader.js";
|
|
3
|
-
import {
|
|
2
|
+
import { clearInspector, renderInspector } from "../components/structureGraph.js";
|
|
4
3
|
import { escapeHtml, formatNumber } from "../utils/format.js";
|
|
5
4
|
|
|
6
|
-
function renderEntryGroup(title, entries, activeName) {
|
|
5
|
+
function renderEntryGroup(title, entries, activeName, options = {}) {
|
|
6
|
+
const { compact = false, showMeta = true } = options;
|
|
7
|
+
|
|
7
8
|
return `
|
|
8
9
|
<section class="shell-section p-5">
|
|
9
10
|
<div class="mb-4 text-[10px] font-bold uppercase tracking-[0.25em] text-primary-container">
|
|
@@ -17,13 +18,18 @@ function renderEntryGroup(title, entries, activeName) {
|
|
|
17
18
|
.map(
|
|
18
19
|
(entry) => `
|
|
19
20
|
<button
|
|
20
|
-
class="w-full border px-3 py-3 text-left transition-colors ${
|
|
21
|
+
class="w-full border px-3 ${compact ? "py-2.5" : "py-3"} text-left transition-colors ${
|
|
21
22
|
entry.name === activeName
|
|
22
23
|
? "border-primary-container/30 bg-surface-container-high"
|
|
23
24
|
: "border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high"
|
|
24
25
|
}"
|
|
25
26
|
data-action="select-structure-entry"
|
|
26
27
|
data-entry-name="${escapeHtml(entry.name)}"
|
|
28
|
+
${
|
|
29
|
+
entry.type === "table"
|
|
30
|
+
? `data-structure-entry-name="${escapeHtml(entry.name)}"`
|
|
31
|
+
: ""
|
|
32
|
+
}
|
|
27
33
|
type="button"
|
|
28
34
|
>
|
|
29
35
|
<div class="font-mono text-xs ${
|
|
@@ -33,9 +39,15 @@ function renderEntryGroup(title, entries, activeName) {
|
|
|
33
39
|
}">
|
|
34
40
|
${escapeHtml(entry.name)}
|
|
35
41
|
</div>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
${
|
|
43
|
+
showMeta
|
|
44
|
+
? `
|
|
45
|
+
<div class="mt-1 text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">
|
|
46
|
+
${escapeHtml(entry.tableName || entry.type)}
|
|
47
|
+
</div>
|
|
48
|
+
`
|
|
49
|
+
: ""
|
|
50
|
+
}
|
|
39
51
|
</button>
|
|
40
52
|
`
|
|
41
53
|
)
|
|
@@ -50,136 +62,242 @@ function renderEntryGroup(title, entries, activeName) {
|
|
|
50
62
|
`;
|
|
51
63
|
}
|
|
52
64
|
|
|
53
|
-
function
|
|
65
|
+
function renderLoadingInspector() {
|
|
66
|
+
return `
|
|
67
|
+
<div class="structure-graph__panel is-empty">
|
|
68
|
+
<span class="material-symbols-outlined structure-graph__empty-icon">progress_activity</span>
|
|
69
|
+
<div class="structure-graph__title">Loading</div>
|
|
70
|
+
<p class="text-sm leading-7 text-on-surface-variant/55">
|
|
71
|
+
Loading structure metadata for the selected object.
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function renderObjectInspector(detail) {
|
|
54
78
|
if (!detail) {
|
|
55
|
-
return
|
|
56
|
-
<div class="shell-section p-8">
|
|
57
|
-
<p class="text-sm text-on-surface-variant/55">Select a structure object to inspect metadata and relational detail.</p>
|
|
58
|
-
</div>
|
|
59
|
-
`;
|
|
79
|
+
return clearInspector();
|
|
60
80
|
}
|
|
61
81
|
|
|
82
|
+
const visibleColumns = (detail.columns ?? []).filter((column) => column.visible !== false);
|
|
83
|
+
const foreignKeyLinks = (detail.foreignKeys ?? []).reduce(
|
|
84
|
+
(count, foreignKey) => count + (foreignKey.mappings?.length ?? 0),
|
|
85
|
+
0
|
|
86
|
+
);
|
|
87
|
+
|
|
62
88
|
return `
|
|
63
|
-
<
|
|
64
|
-
<div class="
|
|
65
|
-
<div>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
</
|
|
69
|
-
<p class="mt-2 font-mono text-[10px] uppercase tracking-[0.2em] text-on-surface-variant/50">
|
|
70
|
-
${escapeHtml(detail.type ?? "table")}
|
|
71
|
-
</p>
|
|
72
|
-
</div>
|
|
73
|
-
<div class="flex items-center gap-3">
|
|
89
|
+
<div class="structure-graph__panel">
|
|
90
|
+
<div class="space-y-3">
|
|
91
|
+
<div class="structure-graph__eyebrow">Object Inspector</div>
|
|
92
|
+
<div class="structure-graph__title">${escapeHtml(detail.name)}</div>
|
|
93
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
94
|
+
<div class="structure-graph__subtitle">${escapeHtml(detail.type ?? "object")}</div>
|
|
74
95
|
${
|
|
75
|
-
detail.
|
|
96
|
+
detail.tableName
|
|
76
97
|
? `
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
data-to="/data/${encodeURIComponent(detail.name)}"
|
|
81
|
-
type="button"
|
|
82
|
-
>
|
|
83
|
-
Open Data
|
|
84
|
-
</button>
|
|
98
|
+
<div class="structure-graph__subtitle">
|
|
99
|
+
TABLE ${escapeHtml(detail.tableName)}
|
|
100
|
+
</div>
|
|
85
101
|
`
|
|
86
102
|
: ""
|
|
87
103
|
}
|
|
88
|
-
<div class="flex h-12 w-12 items-center justify-center bg-primary-container/10">
|
|
89
|
-
<span class="material-symbols-outlined text-2xl text-primary-container">account_tree</span>
|
|
90
|
-
</div>
|
|
91
104
|
</div>
|
|
92
105
|
</div>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
</div>
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
106
|
+
|
|
107
|
+
<div class="structure-graph__summary">
|
|
108
|
+
<div class="structure-graph__summary-card">
|
|
109
|
+
<div class="structure-graph__summary-label">Columns</div>
|
|
110
|
+
<div class="structure-graph__summary-value">${escapeHtml(
|
|
111
|
+
formatNumber(visibleColumns.length)
|
|
112
|
+
)}</div>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="structure-graph__summary-card">
|
|
115
|
+
<div class="structure-graph__summary-label">FK Links</div>
|
|
116
|
+
<div class="structure-graph__summary-value">${escapeHtml(
|
|
117
|
+
formatNumber(foreignKeyLinks)
|
|
118
|
+
)}</div>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="structure-graph__summary-card">
|
|
121
|
+
<div class="structure-graph__summary-label">Indexes</div>
|
|
122
|
+
<div class="structure-graph__summary-value">${escapeHtml(
|
|
123
|
+
formatNumber(detail.indexes?.length ?? 0)
|
|
124
|
+
)}</div>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="structure-graph__summary-card">
|
|
127
|
+
<div class="structure-graph__summary-label">Triggers</div>
|
|
128
|
+
<div class="structure-graph__summary-value">${escapeHtml(
|
|
129
|
+
formatNumber(detail.triggers?.length ?? 0)
|
|
130
|
+
)}</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
${
|
|
135
|
+
visibleColumns.length
|
|
136
|
+
? `
|
|
137
|
+
<section class="structure-graph__section">
|
|
138
|
+
<div class="structure-graph__section-title">Columns</div>
|
|
139
|
+
<div class="structure-graph__column-list">
|
|
140
|
+
${visibleColumns
|
|
141
|
+
.map(
|
|
142
|
+
(column) => `
|
|
143
|
+
<div class="structure-graph__column-row">
|
|
144
|
+
<div class="min-w-0 space-y-1">
|
|
145
|
+
<div class="structure-graph__column-name">${escapeHtml(
|
|
146
|
+
column.name
|
|
147
|
+
)}</div>
|
|
148
|
+
<div class="structure-graph__column-type">${escapeHtml(
|
|
149
|
+
column.declaredType || column.affinity || "BLOB"
|
|
150
|
+
)}</div>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="structure-graph__column-flags">
|
|
153
|
+
${
|
|
154
|
+
column.primaryKeyPosition > 0
|
|
155
|
+
? `<span class="structure-graph__flag is-key">PK${
|
|
156
|
+
column.primaryKeyPosition > 1
|
|
157
|
+
? ` ${escapeHtml(String(column.primaryKeyPosition))}`
|
|
158
|
+
: ""
|
|
159
|
+
}</span>`
|
|
160
|
+
: ""
|
|
161
|
+
}
|
|
162
|
+
<span class="structure-graph__flag is-nullable">${
|
|
163
|
+
column.notNull ? "NOT NULL" : "NULLABLE"
|
|
164
|
+
}</span>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
`
|
|
168
|
+
)
|
|
169
|
+
.join("")}
|
|
170
|
+
</div>
|
|
171
|
+
</section>
|
|
172
|
+
`
|
|
173
|
+
: ""
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
<section class="structure-graph__section">
|
|
177
|
+
<div class="structure-graph__section-title">DDL</div>
|
|
178
|
+
<pre class="structure-graph__ddl custom-scrollbar">${escapeHtml(
|
|
179
|
+
detail.ddl || "No DDL available."
|
|
180
|
+
)}</pre>
|
|
181
|
+
</section>
|
|
182
|
+
</div>
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function renderGraphSurface(structure, selectedName, detail, detailLoading) {
|
|
187
|
+
const graph = structure?.graph ?? { tables: [], relationshipCount: 0 };
|
|
188
|
+
const selectedGraphTable =
|
|
189
|
+
graph.tables?.find((table) => table.name === selectedName && table.type === "table") ?? null;
|
|
190
|
+
const inspectorMarkup = selectedGraphTable
|
|
191
|
+
? renderInspector(selectedGraphTable)
|
|
192
|
+
: detailLoading
|
|
193
|
+
? renderLoadingInspector()
|
|
194
|
+
: detail
|
|
195
|
+
? renderObjectInspector(detail)
|
|
196
|
+
: clearInspector();
|
|
197
|
+
|
|
198
|
+
return `
|
|
199
|
+
<section class="structure-graph" data-structure-graph-root>
|
|
200
|
+
<div class="structure-graph__toolbar">
|
|
201
|
+
<div class="structure-graph__toolbar-main">
|
|
202
|
+
<label class="structure-graph__search" data-structure-graph-search>
|
|
203
|
+
<span class="material-symbols-outlined structure-graph__search-icon">search</span>
|
|
204
|
+
<input
|
|
205
|
+
class="structure-graph__search-input"
|
|
206
|
+
data-structure-graph-search-input
|
|
207
|
+
placeholder="Find table"
|
|
208
|
+
spellcheck="false"
|
|
209
|
+
type="search"
|
|
210
|
+
/>
|
|
211
|
+
</label>
|
|
212
|
+
<div class="structure-graph__toolbar-meta">
|
|
213
|
+
${escapeHtml(formatNumber(graph.tables?.length ?? 0))} TABLES //
|
|
214
|
+
${escapeHtml(formatNumber(graph.relationshipCount ?? 0))} RELATIONSHIPS
|
|
146
215
|
</div>
|
|
147
216
|
</div>
|
|
148
|
-
<div class="
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
217
|
+
<div class="structure-graph__toolbar-actions">
|
|
218
|
+
<button
|
|
219
|
+
class="structure-graph__button"
|
|
220
|
+
data-structure-graph-action="fit"
|
|
221
|
+
type="button"
|
|
222
|
+
>
|
|
223
|
+
<span class="material-symbols-outlined text-sm">fit_screen</span>
|
|
224
|
+
Fit Graph
|
|
225
|
+
</button>
|
|
226
|
+
<button
|
|
227
|
+
class="structure-graph__button"
|
|
228
|
+
data-structure-graph-action="relayout"
|
|
229
|
+
type="button"
|
|
230
|
+
>
|
|
231
|
+
<span class="material-symbols-outlined text-sm">device_hub</span>
|
|
232
|
+
Recalculate Layout
|
|
233
|
+
</button>
|
|
234
|
+
<button
|
|
235
|
+
class="structure-graph__button"
|
|
236
|
+
data-structure-graph-action="clear"
|
|
237
|
+
type="button"
|
|
238
|
+
>
|
|
239
|
+
<span class="material-symbols-outlined text-sm">close</span>
|
|
240
|
+
Clear Selection
|
|
241
|
+
</button>
|
|
242
|
+
<button
|
|
243
|
+
class="structure-graph__button is-disabled"
|
|
244
|
+
data-structure-graph-action="open-data"
|
|
245
|
+
disabled
|
|
246
|
+
type="button"
|
|
247
|
+
>
|
|
248
|
+
<span class="material-symbols-outlined text-sm">table_rows</span>
|
|
249
|
+
Open Data
|
|
250
|
+
</button>
|
|
251
|
+
<button
|
|
252
|
+
class="structure-graph__button"
|
|
253
|
+
data-structure-graph-action="toggle-inspector"
|
|
254
|
+
type="button"
|
|
255
|
+
>
|
|
256
|
+
<span class="material-symbols-outlined text-sm">right_panel_close</span>
|
|
257
|
+
Hide Inspector
|
|
258
|
+
</button>
|
|
153
259
|
</div>
|
|
154
260
|
</div>
|
|
261
|
+
|
|
262
|
+
<div class="structure-graph__workspace">
|
|
263
|
+
<div class="structure-graph__canvas-shell">
|
|
264
|
+
<div class="structure-graph__canvas" data-structure-graph-canvas></div>
|
|
265
|
+
<div
|
|
266
|
+
class="structure-graph__empty"
|
|
267
|
+
data-structure-graph-empty
|
|
268
|
+
${graph.tables?.length ? "hidden" : ""}
|
|
269
|
+
>
|
|
270
|
+
<span class="material-symbols-outlined structure-graph__empty-icon">account_tree</span>
|
|
271
|
+
<div class="structure-graph__title">No Tables</div>
|
|
272
|
+
<p class="text-sm leading-7 text-on-surface-variant/55">
|
|
273
|
+
The active database does not expose table metadata for the schema graph.
|
|
274
|
+
</p>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<aside
|
|
279
|
+
class="structure-graph__inspector custom-scrollbar"
|
|
280
|
+
data-structure-graph-inspector
|
|
281
|
+
>
|
|
282
|
+
${inspectorMarkup}
|
|
283
|
+
</aside>
|
|
284
|
+
</div>
|
|
155
285
|
</section>
|
|
156
286
|
`;
|
|
157
287
|
}
|
|
158
288
|
|
|
159
289
|
export function renderStructureView(state) {
|
|
160
290
|
const structure = state.structure.data;
|
|
161
|
-
const detail =
|
|
162
|
-
|
|
163
|
-
? [
|
|
164
|
-
{ label: "Tables", value: formatNumber(structure.grouped.tables.length) },
|
|
165
|
-
{ label: "Views", value: formatNumber(structure.grouped.views.length) },
|
|
166
|
-
{ label: "Indexes", value: formatNumber(structure.grouped.indexes.length) },
|
|
167
|
-
{
|
|
168
|
-
label: "Triggers",
|
|
169
|
-
value: formatNumber(structure.grouped.triggers.length),
|
|
170
|
-
accent: true,
|
|
171
|
-
},
|
|
172
|
-
]
|
|
173
|
-
: [];
|
|
291
|
+
const detail =
|
|
292
|
+
state.structure.detail?.name === state.structure.selectedName ? state.structure.detail : null;
|
|
174
293
|
|
|
175
294
|
return {
|
|
176
295
|
main: `
|
|
177
296
|
<section class="view-surface min-h-full bg-surface-container">
|
|
178
|
-
<div class="view-frame
|
|
297
|
+
<div class="view-frame space-y-8">
|
|
179
298
|
${renderPageHeader({
|
|
180
|
-
eyebrow: "Structure // sqlite_master + PRAGMA",
|
|
181
299
|
title: "Structure",
|
|
182
|
-
subtitle: "
|
|
300
|
+
subtitle: "Schema graph, foreign-key paths, raw DDL, and object metadata",
|
|
183
301
|
})}
|
|
184
302
|
|
|
185
303
|
${
|
|
@@ -203,15 +321,13 @@ export function renderStructureView(state) {
|
|
|
203
321
|
`
|
|
204
322
|
: structure
|
|
205
323
|
? `
|
|
206
|
-
<
|
|
207
|
-
${counts.map((metric) => renderMetricCard(metric)).join("")}
|
|
208
|
-
</div>
|
|
209
|
-
<section class="grid grid-cols-1 gap-6 xl:grid-cols-[0.45fr_1.55fr]">
|
|
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)]">
|
|
210
325
|
<div class="space-y-6">
|
|
211
326
|
${renderEntryGroup(
|
|
212
327
|
"Tables",
|
|
213
328
|
structure.grouped.tables,
|
|
214
|
-
state.structure.selectedName
|
|
329
|
+
state.structure.selectedName,
|
|
330
|
+
{ compact: true, showMeta: false }
|
|
215
331
|
)}
|
|
216
332
|
${renderEntryGroup(
|
|
217
333
|
"Views",
|
|
@@ -221,7 +337,8 @@ export function renderStructureView(state) {
|
|
|
221
337
|
${renderEntryGroup(
|
|
222
338
|
"Indexes",
|
|
223
339
|
structure.grouped.indexes,
|
|
224
|
-
state.structure.selectedName
|
|
340
|
+
state.structure.selectedName,
|
|
341
|
+
{ compact: true, showMeta: false }
|
|
225
342
|
)}
|
|
226
343
|
${renderEntryGroup(
|
|
227
344
|
"Triggers",
|
|
@@ -229,7 +346,12 @@ export function renderStructureView(state) {
|
|
|
229
346
|
state.structure.selectedName
|
|
230
347
|
)}
|
|
231
348
|
</div>
|
|
232
|
-
${
|
|
349
|
+
${renderGraphSurface(
|
|
350
|
+
structure,
|
|
351
|
+
state.structure.selectedName,
|
|
352
|
+
detail,
|
|
353
|
+
state.structure.detailLoading
|
|
354
|
+
)}
|
|
233
355
|
</section>
|
|
234
356
|
`
|
|
235
357
|
: ""
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sqlite-hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "SQLite-only local management app backend and SPA shell",
|
|
5
5
|
"main": "server/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"better-sqlite3": "^11.8.1",
|
|
16
|
+
"cytoscape": "^3.33.2",
|
|
17
|
+
"cytoscape-elk": "^2.3.0",
|
|
18
|
+
"elkjs": "^0.11.1",
|
|
16
19
|
"express": "^4.21.2"
|
|
17
20
|
}
|
|
18
21
|
}
|