sqlite-hub 0.3.1 → 0.4.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 +12 -6
- package/changelog.md +12 -0
- package/{js → frontend/js}/api.js +7 -0
- package/{js → frontend/js}/app.js +87 -3
- package/{js → frontend/js}/components/connectionCard.js +8 -9
- package/frontend/js/components/connectionLogo.js +33 -0
- package/{js → frontend/js}/components/emptyState.js +25 -11
- package/{js → frontend/js}/components/modal.js +131 -0
- package/{js → frontend/js}/components/rowEditorPanel.js +63 -33
- package/{js → frontend/js}/components/sidebar.js +8 -3
- package/{js → frontend/js}/store.js +236 -1
- package/{js → frontend/js}/views/data.js +106 -9
- package/{js → frontend/js}/views/editor.js +4 -0
- package/{js → frontend/js}/views/structure.js +10 -12
- package/{styles → frontend/styles}/structure-graph.css +5 -10
- package/package.json +3 -3
- 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 +14 -0
- package/server/server.js +29 -6
- package/server/services/sqlite/connectionManager.js +68 -33
- package/server/services/sqlite/dataBrowserService.js +30 -0
- package/server/services/storage/appStateStore.js +159 -20
- 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 +0 -0
- /package/{js → frontend/js}/components/dataGrid.js +0 -0
- /package/{js → frontend/js}/components/metricCard.js +0 -0
- /package/{js → frontend/js}/components/pageHeader.js +0 -0
- /package/{js → frontend/js}/components/queryEditor.js +0 -0
- /package/{js → frontend/js}/components/queryResults.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}/utils/format.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/overview.js +0 -0
- /package/{js → frontend/js}/views/settings.js +0 -0
- /package/{styles → frontend/styles}/base.css +0 -0
- /package/{styles → frontend/styles}/components.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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { escapeHtml } from "../utils/format.js";
|
|
2
|
+
import { renderConnectionLogo } from "./connectionLogo.js";
|
|
2
3
|
|
|
3
4
|
const sidebarItems = [
|
|
4
5
|
{ label: "Connections", href: "#/connections", key: "connections", icon: "database" },
|
|
@@ -40,9 +41,13 @@ export function renderSidebar(state) {
|
|
|
40
41
|
</nav>
|
|
41
42
|
<div class="sidebar-footer">
|
|
42
43
|
<div class="sidebar-footer-card">
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
${renderConnectionLogo(activeConnection, {
|
|
45
|
+
containerClass:
|
|
46
|
+
"sidebar-footer-mark overflow-hidden bg-primary-container text-on-primary",
|
|
47
|
+
imageClassName: "h-full w-full object-cover",
|
|
48
|
+
iconClassName: "text-[15px]",
|
|
49
|
+
icon: "memory",
|
|
50
|
+
})}
|
|
46
51
|
<div class="min-w-0">
|
|
47
52
|
<p class="truncate text-[10px] font-bold text-on-surface">
|
|
48
53
|
${escapeHtml(activeConnection?.label ?? "NO_ACTIVE_DATABASE")}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as api from "./api.js";
|
|
2
|
-
import { inferStatusTone } from "./utils/format.js";
|
|
2
|
+
import { formatCellValue, inferStatusTone, truncateMiddle } from "./utils/format.js";
|
|
3
3
|
|
|
4
4
|
const listeners = new Set();
|
|
5
5
|
const DEFAULT_SETTINGS = {
|
|
@@ -45,8 +45,11 @@ const state = {
|
|
|
45
45
|
loading: false,
|
|
46
46
|
tableLoading: false,
|
|
47
47
|
saving: false,
|
|
48
|
+
deleting: false,
|
|
48
49
|
page: 1,
|
|
49
50
|
pageSize: 50,
|
|
51
|
+
searchQuery: "",
|
|
52
|
+
searchColumn: "",
|
|
50
53
|
selectedRowIndex: null,
|
|
51
54
|
exportLoading: false,
|
|
52
55
|
error: null,
|
|
@@ -64,6 +67,7 @@ const state = {
|
|
|
64
67
|
exportLoading: false,
|
|
65
68
|
selectedRowIndex: null,
|
|
66
69
|
saving: false,
|
|
70
|
+
deleting: false,
|
|
67
71
|
saveError: null,
|
|
68
72
|
},
|
|
69
73
|
structure: {
|
|
@@ -116,6 +120,11 @@ function canEditQueryResult(snapshot = state) {
|
|
|
116
120
|
return Boolean(snapshot.editor.result?.editing?.enabled) && !snapshot.connections.active?.readOnly;
|
|
117
121
|
}
|
|
118
122
|
|
|
123
|
+
function resetDataBrowserSearch() {
|
|
124
|
+
state.dataBrowser.searchQuery = "";
|
|
125
|
+
state.dataBrowser.searchColumn = "";
|
|
126
|
+
}
|
|
127
|
+
|
|
119
128
|
function buildUpdatedEditorResultRow(existingRow, updatedSourceRow, editableColumns) {
|
|
120
129
|
const nextRow = {
|
|
121
130
|
...existingRow,
|
|
@@ -140,6 +149,32 @@ function getCurrentStructureEntry(snapshot = state) {
|
|
|
140
149
|
return entries.find((entry) => entry.name === snapshot.structure.selectedName) ?? null;
|
|
141
150
|
}
|
|
142
151
|
|
|
152
|
+
function buildDeleteRowPreview(fields = []) {
|
|
153
|
+
return fields
|
|
154
|
+
.filter((field) => field && field.label)
|
|
155
|
+
.slice(0, 8)
|
|
156
|
+
.map((field) => {
|
|
157
|
+
const fullValue = formatCellValue(field.value);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
label: String(field.label),
|
|
161
|
+
value: truncateMiddle(fullValue, 96),
|
|
162
|
+
fullValue,
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildFallbackDeleteRowPreview(row) {
|
|
168
|
+
return buildDeleteRowPreview(
|
|
169
|
+
Object.entries(row ?? {})
|
|
170
|
+
.filter(([key]) => key !== "__identity")
|
|
171
|
+
.map(([key, value]) => ({
|
|
172
|
+
label: key,
|
|
173
|
+
value,
|
|
174
|
+
}))
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
143
178
|
function clearRouteSlices() {
|
|
144
179
|
state.overview.error = null;
|
|
145
180
|
state.dataBrowser.error = null;
|
|
@@ -160,6 +195,7 @@ function setMissingDatabaseState() {
|
|
|
160
195
|
state.dataBrowser.selectedTable = null;
|
|
161
196
|
state.dataBrowser.table = null;
|
|
162
197
|
state.dataBrowser.page = 1;
|
|
198
|
+
resetDataBrowserSearch();
|
|
163
199
|
state.dataBrowser.selectedRowIndex = null;
|
|
164
200
|
state.dataBrowser.exportLoading = false;
|
|
165
201
|
state.dataBrowser.error = error;
|
|
@@ -193,6 +229,9 @@ function syncRouteContext() {
|
|
|
193
229
|
if (route.name !== "data" || route.params?.tableName !== state.dataBrowser.selectedTable) {
|
|
194
230
|
state.dataBrowser.page = 1;
|
|
195
231
|
}
|
|
232
|
+
if (route.params?.tableName !== state.dataBrowser.selectedTable) {
|
|
233
|
+
resetDataBrowserSearch();
|
|
234
|
+
}
|
|
196
235
|
state.dataBrowser.selectedRowIndex = null;
|
|
197
236
|
state.dataBrowser.saveError = null;
|
|
198
237
|
}
|
|
@@ -318,6 +357,9 @@ async function loadDataTable(version) {
|
|
|
318
357
|
state.dataBrowser.table = response.data ?? null;
|
|
319
358
|
state.dataBrowser.pageSize = pageSize;
|
|
320
359
|
state.dataBrowser.page = response.data?.page ?? page;
|
|
360
|
+
state.dataBrowser.searchColumn = response.data?.columns?.includes(state.dataBrowser.searchColumn)
|
|
361
|
+
? state.dataBrowser.searchColumn
|
|
362
|
+
: (response.data?.columns?.[0] ?? "");
|
|
321
363
|
state.dataBrowser.selectedRowIndex = null;
|
|
322
364
|
} catch (error) {
|
|
323
365
|
if (version !== routeLoadVersion) {
|
|
@@ -355,6 +397,7 @@ async function loadData(version, route) {
|
|
|
355
397
|
if (requestedTableName && tables.some((table) => table.name === requestedTableName)) {
|
|
356
398
|
if (requestedTableName !== state.dataBrowser.selectedTable) {
|
|
357
399
|
state.dataBrowser.page = 1;
|
|
400
|
+
resetDataBrowserSearch();
|
|
358
401
|
}
|
|
359
402
|
state.dataBrowser.selectedTable = requestedTableName;
|
|
360
403
|
} else if (
|
|
@@ -363,10 +406,12 @@ async function loadData(version, route) {
|
|
|
363
406
|
) {
|
|
364
407
|
state.dataBrowser.selectedTable = tables[0]?.name ?? null;
|
|
365
408
|
state.dataBrowser.page = 1;
|
|
409
|
+
resetDataBrowserSearch();
|
|
366
410
|
}
|
|
367
411
|
|
|
368
412
|
if (!state.dataBrowser.selectedTable) {
|
|
369
413
|
state.dataBrowser.table = null;
|
|
414
|
+
resetDataBrowserSearch();
|
|
370
415
|
state.dataBrowser.selectedRowIndex = null;
|
|
371
416
|
return;
|
|
372
417
|
}
|
|
@@ -482,6 +527,7 @@ function invalidateDatabaseCaches() {
|
|
|
482
527
|
state.dataBrowser.selectedTable = null;
|
|
483
528
|
state.dataBrowser.table = null;
|
|
484
529
|
state.dataBrowser.page = 1;
|
|
530
|
+
resetDataBrowserSearch();
|
|
485
531
|
state.dataBrowser.selectedRowIndex = null;
|
|
486
532
|
state.dataBrowser.exportLoading = false;
|
|
487
533
|
state.dataBrowser.error = null;
|
|
@@ -621,6 +667,69 @@ export function openEditConnectionModal(id) {
|
|
|
621
667
|
emitChange();
|
|
622
668
|
}
|
|
623
669
|
|
|
670
|
+
export function openDeleteDataRowModal(rowIndex) {
|
|
671
|
+
const numericIndex = Number(rowIndex);
|
|
672
|
+
const tableName = state.dataBrowser.selectedTable;
|
|
673
|
+
const row = state.dataBrowser.table?.rows?.[numericIndex];
|
|
674
|
+
const rowPreview = buildDeleteRowPreview(
|
|
675
|
+
(state.dataBrowser.table?.columnMeta ?? [])
|
|
676
|
+
.filter((column) => column.visible)
|
|
677
|
+
.map((column) => ({
|
|
678
|
+
label: column.name,
|
|
679
|
+
value: row[column.name],
|
|
680
|
+
}))
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
if (!tableName || !row?.__identity) {
|
|
684
|
+
pushToast("The selected row could not be loaded.", "alert");
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
state.modal = {
|
|
689
|
+
kind: "delete-row",
|
|
690
|
+
target: "data",
|
|
691
|
+
rowIndex: numericIndex,
|
|
692
|
+
tableName,
|
|
693
|
+
rowLabel: `row ${numericIndex + 1}`,
|
|
694
|
+
rowPreview: rowPreview.length ? rowPreview : buildFallbackDeleteRowPreview(row),
|
|
695
|
+
error: null,
|
|
696
|
+
submitting: false,
|
|
697
|
+
};
|
|
698
|
+
emitChange();
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export function openDeleteEditorRowModal(rowIndex) {
|
|
702
|
+
const numericIndex = Number(rowIndex);
|
|
703
|
+
const tableName = state.editor.result?.editing?.tableName ?? null;
|
|
704
|
+
const row = state.editor.result?.rows?.[numericIndex];
|
|
705
|
+
const columns = state.editor.result?.editing?.columns ?? [];
|
|
706
|
+
const rowPreview = buildDeleteRowPreview(
|
|
707
|
+
columns
|
|
708
|
+
.filter((column) => column.visible !== false)
|
|
709
|
+
.map((column) => ({
|
|
710
|
+
label: column.sourceColumn || column.resultName,
|
|
711
|
+
value: row[column.resultName],
|
|
712
|
+
}))
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
if (!tableName || !row?.__identity || !canEditQueryResult()) {
|
|
716
|
+
pushToast("The selected query result row could not be loaded.", "alert");
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
state.modal = {
|
|
721
|
+
kind: "delete-row",
|
|
722
|
+
target: "editor",
|
|
723
|
+
rowIndex: numericIndex,
|
|
724
|
+
tableName,
|
|
725
|
+
rowLabel: `query row ${numericIndex + 1}`,
|
|
726
|
+
rowPreview: rowPreview.length ? rowPreview : buildFallbackDeleteRowPreview(row),
|
|
727
|
+
error: null,
|
|
728
|
+
submitting: false,
|
|
729
|
+
};
|
|
730
|
+
emitChange();
|
|
731
|
+
}
|
|
732
|
+
|
|
624
733
|
export function closeModal() {
|
|
625
734
|
closeModalInternal();
|
|
626
735
|
}
|
|
@@ -919,6 +1028,20 @@ export function clearDataRowSelection() {
|
|
|
919
1028
|
emitChange();
|
|
920
1029
|
}
|
|
921
1030
|
|
|
1031
|
+
export function setDataSearchQuery(query) {
|
|
1032
|
+
state.dataBrowser.searchQuery = String(query ?? "");
|
|
1033
|
+
state.dataBrowser.selectedRowIndex = null;
|
|
1034
|
+
state.dataBrowser.saveError = null;
|
|
1035
|
+
emitChange();
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
export function setDataSearchColumn(columnName) {
|
|
1039
|
+
state.dataBrowser.searchColumn = String(columnName ?? "");
|
|
1040
|
+
state.dataBrowser.selectedRowIndex = null;
|
|
1041
|
+
state.dataBrowser.saveError = null;
|
|
1042
|
+
emitChange();
|
|
1043
|
+
}
|
|
1044
|
+
|
|
922
1045
|
export async function setDataPage(page) {
|
|
923
1046
|
const numericPage = Number(page);
|
|
924
1047
|
|
|
@@ -992,6 +1115,51 @@ export async function submitDataRowUpdate(rowIndex, values) {
|
|
|
992
1115
|
}
|
|
993
1116
|
}
|
|
994
1117
|
|
|
1118
|
+
export async function submitDataRowDelete(rowIndex, options = {}) {
|
|
1119
|
+
const numericIndex = Number(rowIndex);
|
|
1120
|
+
const tableName = state.dataBrowser.selectedTable;
|
|
1121
|
+
const row = state.dataBrowser.table?.rows?.[numericIndex];
|
|
1122
|
+
const reportErrorToModal = Boolean(options.reportErrorToModal);
|
|
1123
|
+
|
|
1124
|
+
if (!tableName || !row?.__identity) {
|
|
1125
|
+
pushToast("The selected row could not be loaded.", "alert");
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
const shouldStepBackPage =
|
|
1130
|
+
(state.dataBrowser.table?.rows?.length ?? 0) <= 1 && state.dataBrowser.page > 1;
|
|
1131
|
+
|
|
1132
|
+
state.dataBrowser.deleting = true;
|
|
1133
|
+
state.dataBrowser.saveError = null;
|
|
1134
|
+
emitChange();
|
|
1135
|
+
|
|
1136
|
+
try {
|
|
1137
|
+
const response = await api.deleteDataTableRow(tableName, {
|
|
1138
|
+
identity: row.__identity,
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
if (shouldStepBackPage) {
|
|
1142
|
+
state.dataBrowser.page -= 1;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
pushToast(response.message || "Table row deleted.", "success");
|
|
1146
|
+
await loadDataTable(++routeLoadVersion);
|
|
1147
|
+
state.dataBrowser.selectedRowIndex = null;
|
|
1148
|
+
return response.data;
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
if (reportErrorToModal) {
|
|
1151
|
+
withModalError(error);
|
|
1152
|
+
} else {
|
|
1153
|
+
state.dataBrowser.saveError = normalizeError(error);
|
|
1154
|
+
emitChange();
|
|
1155
|
+
}
|
|
1156
|
+
return null;
|
|
1157
|
+
} finally {
|
|
1158
|
+
state.dataBrowser.deleting = false;
|
|
1159
|
+
emitChange();
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
995
1163
|
export async function submitEditorRowUpdate(rowIndex, values) {
|
|
996
1164
|
const numericIndex = Number(rowIndex);
|
|
997
1165
|
const result = state.editor.result;
|
|
@@ -1039,6 +1207,73 @@ export async function submitEditorRowUpdate(rowIndex, values) {
|
|
|
1039
1207
|
}
|
|
1040
1208
|
}
|
|
1041
1209
|
|
|
1210
|
+
export async function submitEditorRowDelete(rowIndex, options = {}) {
|
|
1211
|
+
const numericIndex = Number(rowIndex);
|
|
1212
|
+
const result = state.editor.result;
|
|
1213
|
+
const row = result?.rows?.[numericIndex];
|
|
1214
|
+
const tableName = result?.editing?.tableName ?? null;
|
|
1215
|
+
const reportErrorToModal = Boolean(options.reportErrorToModal);
|
|
1216
|
+
|
|
1217
|
+
if (!tableName || !row?.__identity || !canEditQueryResult()) {
|
|
1218
|
+
pushToast("The selected query result row could not be loaded.", "alert");
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
state.editor.deleting = true;
|
|
1223
|
+
state.editor.saveError = null;
|
|
1224
|
+
emitChange();
|
|
1225
|
+
|
|
1226
|
+
try {
|
|
1227
|
+
const response = await api.deleteDataTableRow(tableName, {
|
|
1228
|
+
identity: row.__identity,
|
|
1229
|
+
});
|
|
1230
|
+
const nextRows = [...(result.rows ?? [])];
|
|
1231
|
+
|
|
1232
|
+
nextRows.splice(numericIndex, 1);
|
|
1233
|
+
state.editor.result = {
|
|
1234
|
+
...result,
|
|
1235
|
+
rows: nextRows,
|
|
1236
|
+
};
|
|
1237
|
+
state.editor.selectedRowIndex = null;
|
|
1238
|
+
invalidateDatabaseCaches();
|
|
1239
|
+
pushToast(response.message || "Query result row deleted.", "success");
|
|
1240
|
+
emitChange();
|
|
1241
|
+
return response.data;
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
if (reportErrorToModal) {
|
|
1244
|
+
withModalError(error);
|
|
1245
|
+
} else {
|
|
1246
|
+
state.editor.saveError = normalizeError(error);
|
|
1247
|
+
emitChange();
|
|
1248
|
+
}
|
|
1249
|
+
return null;
|
|
1250
|
+
} finally {
|
|
1251
|
+
state.editor.deleting = false;
|
|
1252
|
+
emitChange();
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
export async function submitDeleteRowConfirmation() {
|
|
1257
|
+
const modal = state.modal;
|
|
1258
|
+
|
|
1259
|
+
if (modal?.kind !== "delete-row") {
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
startModalSubmission();
|
|
1264
|
+
|
|
1265
|
+
const result =
|
|
1266
|
+
modal.target === "editor"
|
|
1267
|
+
? await submitEditorRowDelete(modal.rowIndex, { reportErrorToModal: true })
|
|
1268
|
+
: await submitDataRowDelete(modal.rowIndex, { reportErrorToModal: true });
|
|
1269
|
+
|
|
1270
|
+
if (result) {
|
|
1271
|
+
closeModalInternal();
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
return result;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1042
1277
|
export async function exportCurrentQueryCsv() {
|
|
1043
1278
|
state.editor.exportLoading = true;
|
|
1044
1279
|
emitChange();
|
|
@@ -168,6 +168,80 @@ function getCellWidthClass(columnName) {
|
|
|
168
168
|
return "max-w-[12rem]";
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
function getFilteredTableRows(table, state) {
|
|
172
|
+
const allRows = table?.rows ?? [];
|
|
173
|
+
const availableColumns = table?.columns ?? [];
|
|
174
|
+
const searchQuery = String(state.dataBrowser.searchQuery ?? "").trim().toLowerCase();
|
|
175
|
+
const activeColumn = availableColumns.includes(state.dataBrowser.searchColumn)
|
|
176
|
+
? state.dataBrowser.searchColumn
|
|
177
|
+
: (availableColumns[0] ?? "");
|
|
178
|
+
|
|
179
|
+
const indexedRows = allRows.map((row, index) => ({
|
|
180
|
+
row,
|
|
181
|
+
index,
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
if (!searchQuery || !activeColumn) {
|
|
185
|
+
return {
|
|
186
|
+
activeColumn,
|
|
187
|
+
filteredRows: indexedRows,
|
|
188
|
+
searchQuery,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
activeColumn,
|
|
194
|
+
searchQuery,
|
|
195
|
+
filteredRows: indexedRows.filter(({ row }) =>
|
|
196
|
+
formatCellValue(row[activeColumn]).toLowerCase().includes(searchQuery)
|
|
197
|
+
),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function renderTableSearchBar(table, state, activeColumn, filteredRowCount) {
|
|
202
|
+
const columns = table?.columns ?? [];
|
|
203
|
+
|
|
204
|
+
if (!table || !columns.length) {
|
|
205
|
+
return "";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return `
|
|
209
|
+
<div class="flex flex-wrap items-center gap-3 border-b border-outline-variant/10 bg-surface-container-low px-6 py-4">
|
|
210
|
+
<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">
|
|
211
|
+
<span class="material-symbols-outlined text-base text-on-surface-variant/55">search</span>
|
|
212
|
+
<input
|
|
213
|
+
class="min-w-0 flex-1 bg-transparent text-sm text-on-surface outline-none placeholder:text-on-surface-variant/40"
|
|
214
|
+
data-bind="data-search-query"
|
|
215
|
+
placeholder="Filter current page..."
|
|
216
|
+
type="search"
|
|
217
|
+
value="${escapeHtml(state.dataBrowser.searchQuery ?? "")}"
|
|
218
|
+
/>
|
|
219
|
+
</label>
|
|
220
|
+
<select
|
|
221
|
+
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"
|
|
222
|
+
data-bind="data-search-column"
|
|
223
|
+
>
|
|
224
|
+
${columns
|
|
225
|
+
.map(
|
|
226
|
+
(columnName) => `
|
|
227
|
+
<option value="${escapeHtml(columnName)}" ${
|
|
228
|
+
columnName === activeColumn ? "selected" : ""
|
|
229
|
+
}>
|
|
230
|
+
${escapeHtml(columnName)}
|
|
231
|
+
</option>
|
|
232
|
+
`
|
|
233
|
+
)
|
|
234
|
+
.join("")}
|
|
235
|
+
</select>
|
|
236
|
+
<div class="text-[10px] font-mono tracking-[0.14em] text-on-surface-variant/55">
|
|
237
|
+
${escapeHtml(formatNumber(filteredRowCount))} match${
|
|
238
|
+
filteredRowCount === 1 ? "" : "es"
|
|
239
|
+
} on this page
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
`;
|
|
243
|
+
}
|
|
244
|
+
|
|
171
245
|
function renderTableSurface(state) {
|
|
172
246
|
const table = state.dataBrowser.table;
|
|
173
247
|
|
|
@@ -197,10 +271,11 @@ function renderTableSurface(state) {
|
|
|
197
271
|
`;
|
|
198
272
|
}
|
|
199
273
|
|
|
274
|
+
const { activeColumn, filteredRows, searchQuery } = getFilteredTableRows(table, state);
|
|
200
275
|
const columns = (table.columns ?? []).map((columnName) => ({
|
|
201
276
|
label: escapeHtml(columnName),
|
|
202
277
|
headerClassName:
|
|
203
|
-
"border-b border-primary-container/20 px-4 py-3 text-[10px] font-bold
|
|
278
|
+
"border-b border-primary-container/20 px-4 py-3 text-[10px] font-bold tracking-[0.08em] text-primary-container",
|
|
204
279
|
cellClassName: "px-4 py-2 align-top text-[11px] text-on-surface",
|
|
205
280
|
render: (row) => {
|
|
206
281
|
const value = formatCellValue(row[columnName]);
|
|
@@ -219,25 +294,35 @@ function renderTableSurface(state) {
|
|
|
219
294
|
const fromRow = totalRows === 0 ? 0 : (table.offset ?? 0) + 1;
|
|
220
295
|
const toRow = totalRows === 0 ? 0 : Math.min((table.offset ?? 0) + (table.rows?.length ?? 0), totalRows);
|
|
221
296
|
const pageSizes = [25, 50, 100];
|
|
297
|
+
const filteredRowCount = filteredRows.length;
|
|
298
|
+
const hasActiveSearch = Boolean(searchQuery);
|
|
222
299
|
|
|
223
300
|
return `
|
|
224
301
|
<div class="flex flex-1 min-h-0 flex-col bg-surface-container-lowest">
|
|
302
|
+
${renderTableSearchBar(table, state, activeColumn, filteredRowCount)}
|
|
225
303
|
<div class="custom-scrollbar flex-1 overflow-auto">
|
|
226
304
|
${renderDataGrid({
|
|
227
305
|
columns,
|
|
228
|
-
rows:
|
|
306
|
+
rows: filteredRows.map(({ row }) => row),
|
|
229
307
|
tableClass: "min-w-full border-collapse text-left font-mono text-xs",
|
|
230
308
|
theadClass: "sticky top-0 z-10 bg-surface-container-highest",
|
|
231
309
|
tbodyClass: "divide-y divide-outline-variant/5",
|
|
232
|
-
getRowClass: (_,
|
|
233
|
-
|
|
234
|
-
|
|
310
|
+
getRowClass: (_, filteredIndex) => {
|
|
311
|
+
const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
|
|
312
|
+
|
|
313
|
+
return `${
|
|
314
|
+
state.dataBrowser.selectedRowIndex === rowIndex
|
|
235
315
|
? "bg-surface-bright"
|
|
236
|
-
:
|
|
316
|
+
: filteredIndex % 2 === 0
|
|
237
317
|
? "bg-surface-container-low"
|
|
238
318
|
: "bg-surface-container-lowest"
|
|
239
|
-
} cursor-pointer transition-colors hover:bg-surface-container-high
|
|
240
|
-
|
|
319
|
+
} cursor-pointer transition-colors hover:bg-surface-container-high`;
|
|
320
|
+
},
|
|
321
|
+
getRowAttrs: (_, filteredIndex) => {
|
|
322
|
+
const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
|
|
323
|
+
|
|
324
|
+
return `data-action="select-data-row" data-row-index="${rowIndex}"`;
|
|
325
|
+
},
|
|
241
326
|
})}
|
|
242
327
|
${
|
|
243
328
|
!table.rows?.length
|
|
@@ -248,6 +333,14 @@ function renderTableSurface(state) {
|
|
|
248
333
|
</p>
|
|
249
334
|
</div>
|
|
250
335
|
`
|
|
336
|
+
: !filteredRowCount
|
|
337
|
+
? `
|
|
338
|
+
<div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10">
|
|
339
|
+
<p class="font-mono text-[10px] tracking-[0.18em] text-on-surface-variant/40">
|
|
340
|
+
${hasActiveSearch ? "No matching rows on this page." : "No rows available."}
|
|
341
|
+
</p>
|
|
342
|
+
</div>
|
|
343
|
+
`
|
|
251
344
|
: ""
|
|
252
345
|
}
|
|
253
346
|
</div>
|
|
@@ -255,7 +348,7 @@ function renderTableSurface(state) {
|
|
|
255
348
|
<div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
256
349
|
showing ${escapeHtml(formatNumber(fromRow))}-${escapeHtml(formatNumber(toRow))} of ${escapeHtml(
|
|
257
350
|
formatNumber(totalRows)
|
|
258
|
-
)} rows
|
|
351
|
+
)} rows${hasActiveSearch ? ` // ${escapeHtml(formatNumber(filteredRowCount))} visible on this page` : ""}
|
|
259
352
|
</div>
|
|
260
353
|
<div class="flex flex-wrap items-center gap-4">
|
|
261
354
|
<div class="flex items-center gap-2">
|
|
@@ -380,6 +473,10 @@ function renderDataRowEditorPanel(state) {
|
|
|
380
473
|
})),
|
|
381
474
|
saveError: state.dataBrowser.saveError,
|
|
382
475
|
saving: state.dataBrowser.saving,
|
|
476
|
+
deleting: state.dataBrowser.deleting,
|
|
477
|
+
deleteAction: "delete-data-row",
|
|
478
|
+
deleteRowIndex: rowIndex,
|
|
479
|
+
deleteEnabled: Boolean(row.__identity),
|
|
383
480
|
reloadAction: "reload-data-route",
|
|
384
481
|
});
|
|
385
482
|
}
|
|
@@ -192,6 +192,10 @@ function renderEditorRowPanel(state) {
|
|
|
192
192
|
})),
|
|
193
193
|
saveError: state.editor.saveError,
|
|
194
194
|
saving: state.editor.saving,
|
|
195
|
+
deleting: state.editor.deleting,
|
|
196
|
+
deleteAction: "delete-editor-row",
|
|
197
|
+
deleteRowIndex: rowIndex,
|
|
198
|
+
deleteEnabled: editingState.enabled && Boolean(row.__identity),
|
|
195
199
|
});
|
|
196
200
|
}
|
|
197
201
|
|
|
@@ -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,
|
|
@@ -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.4.0",
|
|
4
4
|
"description": "SQLite-only local management app backend and SPA shell",
|
|
5
5
|
"main": "server/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/sqlite-hub.js",
|
|
11
|
-
"dev": "node --watch server/server.js",
|
|
12
|
-
"publish": "bash publish_brew.sh && bash publish_npm.sh"
|
|
11
|
+
"dev": "node --watch server/server.js --port:4180",
|
|
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.
|