db-model-router 1.0.6 → 1.0.7
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 +150 -11
- package/TODO.md +0 -15
- package/db-manager/.dbmanager.sqlite +0 -0
- package/db-manager/README.md +223 -0
- package/db-manager/adapter-proxy.js +361 -0
- package/db-manager/demo/cockroachdb.env +6 -0
- package/db-manager/demo/demo.sqlite +0 -0
- package/db-manager/demo/dynamodb.env +7 -0
- package/db-manager/demo/mongodb.env +4 -0
- package/db-manager/demo/mssql.env +6 -0
- package/db-manager/demo/mysql.env +6 -0
- package/db-manager/demo/oracle.env +6 -0
- package/db-manager/demo/postgres.env +6 -0
- package/db-manager/demo/redis.env +4 -0
- package/db-manager/demo/seeds/cockroachdb.sql +32 -0
- package/db-manager/demo/seeds/mssql.sql +32 -0
- package/db-manager/demo/seeds/mysql.sql +32 -0
- package/db-manager/demo/seeds/oracle.sql +43 -0
- package/db-manager/demo/seeds/postgres.sql +32 -0
- package/db-manager/demo/seeds/sqlite3.sql +32 -0
- package/db-manager/demo/sqlite3.env +2 -0
- package/db-manager/metadata-db.js +170 -0
- package/db-manager/public/.gitkeep +1 -0
- package/db-manager/public/css/style.css +1413 -0
- package/db-manager/public/js/app.js +1370 -0
- package/db-manager/routes/api.js +388 -0
- package/db-manager/routes/views.js +61 -0
- package/db-manager/server.js +39 -0
- package/db-manager/utils/build-filter-config.js +18 -0
- package/db-manager/utils/csv-export.js +59 -0
- package/db-manager/utils/export-filename.js +39 -0
- package/db-manager/utils/filter-tables.js +20 -0
- package/db-manager/utils/parse-filters.js +93 -0
- package/db-manager/utils/sort-state.js +35 -0
- package/db-manager/views/.gitkeep +1 -0
- package/db-manager/views/dashboard.ejs +53 -0
- package/db-manager/views/history.ejs +52 -0
- package/db-manager/views/index.ejs +35 -0
- package/db-manager/views/layout.ejs +31 -0
- package/db-manager/views/partials/data-panel.ejs +74 -0
- package/db-manager/views/partials/header.ejs +36 -0
- package/db-manager/views/partials/sidebar.ejs +30 -0
- package/db-manager/views/query.ejs +58 -0
- package/dbmr.schema.json +22 -44
- package/demo/.dockerignore +7 -0
- package/demo/.env.example +14 -0
- package/demo/Dockerfile +20 -0
- package/demo/app.js +39 -0
- package/demo/commons/add_migration.js +43 -0
- package/demo/commons/db.js +28 -0
- package/demo/commons/migrate.js +68 -0
- package/demo/commons/modules.js +18 -0
- package/demo/commons/password.js +36 -0
- package/demo/commons/security.js +30 -0
- package/demo/commons/session.js +13 -0
- package/demo/commons/webhook.js +81 -0
- package/demo/dbmr.schema.json +338 -0
- package/demo/middleware/authenticate.js +14 -0
- package/demo/middleware/hasPermission.js +30 -0
- package/demo/middleware/logger.js +67 -0
- package/demo/middleware/tenantIsolation.js +17 -0
- package/demo/migrations/20260509170349_create_migrations_table.sql +6 -0
- package/demo/migrations/20260509170349_create_saas_tables.sql +69 -0
- package/demo/migrations/20260509170349_create_tables.sql +193 -0
- package/demo/models/addresses.js +24 -0
- package/demo/models/cart_items.js +20 -0
- package/demo/models/carts.js +18 -0
- package/demo/models/categories.js +22 -0
- package/demo/models/coupons.js +25 -0
- package/demo/models/index.js +43 -0
- package/demo/models/order_items.js +23 -0
- package/demo/models/orders.js +27 -0
- package/demo/models/payments.js +23 -0
- package/demo/models/product_images.js +20 -0
- package/demo/models/product_reviews.js +22 -0
- package/demo/models/product_variants.js +22 -0
- package/demo/models/products.js +32 -0
- package/demo/models/role_permissions.js +17 -0
- package/demo/models/roles.js +17 -0
- package/demo/models/shipments.js +21 -0
- package/demo/models/tenants.js +18 -0
- package/demo/models/users.js +23 -0
- package/demo/models/webhook_logs.js +22 -0
- package/demo/models/webhooks.js +19 -0
- package/demo/models/wishlists.js +17 -0
- package/demo/openapi.json +7000 -0
- package/demo/package-lock.json +2810 -0
- package/demo/package.json +43 -0
- package/demo/routes/addresses/index.js +6 -0
- package/demo/routes/auth/index.js +55 -0
- package/demo/routes/carts/cart_items/index.js +7 -0
- package/demo/routes/carts/index.js +6 -0
- package/demo/routes/categories/index.js +6 -0
- package/demo/routes/coupons/index.js +6 -0
- package/demo/routes/docs.js +18 -0
- package/demo/routes/health.js +35 -0
- package/demo/routes/index.js +54 -0
- package/demo/routes/orders/index.js +6 -0
- package/demo/routes/orders/order_items/index.js +7 -0
- package/demo/routes/orders/payments/index.js +7 -0
- package/demo/routes/orders/shipments/index.js +7 -0
- package/demo/routes/products/index.js +6 -0
- package/demo/routes/products/product_images/index.js +7 -0
- package/demo/routes/products/product_reviews/index.js +7 -0
- package/demo/routes/products/product_variants/index.js +7 -0
- package/demo/routes/roles/index.js +75 -0
- package/demo/routes/roles/permissions/index.js +47 -0
- package/demo/routes/tenants/index.js +45 -0
- package/demo/routes/users/index.js +45 -0
- package/demo/routes/wishlists/index.js +6 -0
- package/demo/seeds/saas-seed.js +329 -0
- package/docker-compose.yml +61 -0
- package/package.json +120 -113
- package/scripts/demo-create.js +1 -1
- package/skill/SKILL.md +119 -3
- package/src/cli/commands/db-manager.js +134 -0
- package/src/cli/commands/generate.js +106 -60
- package/src/cli/commands/help.js +0 -1
- package/src/cli/generate-route.js +60 -21
- package/src/cli/generate-saas-structure.js +122 -0
- package/src/cli/init/generators.js +6 -0
- package/src/cli/init.js +8 -0
- package/src/cli/main.js +8 -1
- package/src/cli/saas/generate-saas-middleware.js +108 -0
- package/src/cli/saas/generate-saas-migrations.js +480 -0
- package/src/cli/saas/generate-saas-models.js +211 -0
- package/src/cli/saas/generate-saas-openapi.js +419 -0
- package/src/cli/saas/generate-saas-routes.js +435 -0
- package/src/cli/saas/generate-saas-seeds.js +243 -0
- package/src/cli/saas/generate-saas-utils.js +176 -0
- package/src/commons/kafka.js +139 -0
- package/src/commons/model.js +29 -9
- package/src/index.js +2 -0
- package/src/mssql/db.js +41 -3
- package/src/mysql/db.js +3 -0
- package/src/postgres/db.js +6 -0
- package/src/cli/generate-db-manager.js +0 -1573
|
@@ -0,0 +1,1370 @@
|
|
|
1
|
+
/* DB Manager - Client-Side Application */
|
|
2
|
+
/* eslint-env browser */
|
|
3
|
+
(function () {
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
// === State ===
|
|
7
|
+
var state = {
|
|
8
|
+
tables: [],
|
|
9
|
+
activeTable: null,
|
|
10
|
+
schema: null,
|
|
11
|
+
rows: [],
|
|
12
|
+
totalCount: 0,
|
|
13
|
+
page: 0,
|
|
14
|
+
limit: 30,
|
|
15
|
+
selectedKeys: [],
|
|
16
|
+
pkColumn: null,
|
|
17
|
+
sortColumn: null,
|
|
18
|
+
sortDir: null,
|
|
19
|
+
filters: [],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// === DOM References ===
|
|
23
|
+
var tableSearchInput = document.querySelector(".table-search");
|
|
24
|
+
var tableList = document.querySelector(".table-list");
|
|
25
|
+
var historyList = document.querySelector(".history-list");
|
|
26
|
+
var columnHeaders = document.querySelector(".column-headers");
|
|
27
|
+
var dataRows = document.querySelector(".data-rows");
|
|
28
|
+
var btnAdd = document.querySelector(".btn-add");
|
|
29
|
+
var btnDelete = document.querySelector(".btn-delete");
|
|
30
|
+
var btnExport = document.querySelector(".btn-export");
|
|
31
|
+
var btnFilter = document.querySelector(".btn-filter");
|
|
32
|
+
var btnPrev = document.querySelector(".btn-prev");
|
|
33
|
+
var btnNext = document.querySelector(".btn-next");
|
|
34
|
+
var pageInfo = document.querySelector(".page-info");
|
|
35
|
+
var pageSize = document.querySelector(".page-size");
|
|
36
|
+
var filterTagsContainer = document.querySelector(".filter-tags");
|
|
37
|
+
var filterModalOverlay = document.querySelector(".filter-modal-overlay");
|
|
38
|
+
var filterColSelect = document.querySelector(".filter-col-select");
|
|
39
|
+
var filterOpSelect = document.querySelector(".filter-op-select");
|
|
40
|
+
var filterValInput = document.querySelector(".filter-val-input");
|
|
41
|
+
var btnFilterAdd = document.querySelector(".btn-filter-add");
|
|
42
|
+
var filterModalClose = document.querySelector(".filter-modal-close");
|
|
43
|
+
|
|
44
|
+
// === Utility: Filter tables (same logic as db-manager/utils/filter-tables.js) ===
|
|
45
|
+
function filterTables(tables, search) {
|
|
46
|
+
if (!search || search.trim() === "") {
|
|
47
|
+
return tables.slice();
|
|
48
|
+
}
|
|
49
|
+
var needle = search.toLowerCase();
|
|
50
|
+
return tables.filter(function (table) {
|
|
51
|
+
return table.toLowerCase().indexOf(needle) !== -1;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// === Utility: Format cell value ===
|
|
56
|
+
// Converts ISO timestamps (YYYY-MM-DDTHH:mm:ss.000Z) to YYYY-MM-DD HH:mm:ss (UTC)
|
|
57
|
+
function formatCellValue(val) {
|
|
58
|
+
if (val === null || val === undefined) return "";
|
|
59
|
+
var str = String(val);
|
|
60
|
+
// Match ISO 8601 timestamp pattern
|
|
61
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z?$/.test(str)) {
|
|
62
|
+
var d = new Date(str);
|
|
63
|
+
if (!isNaN(d.getTime())) {
|
|
64
|
+
var y = d.getUTCFullYear();
|
|
65
|
+
var m = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
66
|
+
var day = String(d.getUTCDate()).padStart(2, "0");
|
|
67
|
+
var h = String(d.getUTCHours()).padStart(2, "0");
|
|
68
|
+
var min = String(d.getUTCMinutes()).padStart(2, "0");
|
|
69
|
+
var s = String(d.getUTCSeconds()).padStart(2, "0");
|
|
70
|
+
return y + "-" + m + "-" + day + " " + h + ":" + min + ":" + s;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return str;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// === Utility: Debounce ===
|
|
77
|
+
function debounce(fn, delay) {
|
|
78
|
+
var timer = null;
|
|
79
|
+
return function () {
|
|
80
|
+
var context = this;
|
|
81
|
+
var args = arguments;
|
|
82
|
+
if (timer) clearTimeout(timer);
|
|
83
|
+
timer = setTimeout(function () {
|
|
84
|
+
fn.apply(context, args);
|
|
85
|
+
}, delay);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// === Utility: Encode filter value with operator prefix for library syntax ===
|
|
90
|
+
function encodeFilterValue(operator, value) {
|
|
91
|
+
switch (operator) {
|
|
92
|
+
case "=":
|
|
93
|
+
return encodeURIComponent(value);
|
|
94
|
+
case "!=":
|
|
95
|
+
return encodeURIComponent("!" + value);
|
|
96
|
+
case ">":
|
|
97
|
+
return encodeURIComponent(">" + value);
|
|
98
|
+
case ">=":
|
|
99
|
+
return encodeURIComponent(">=" + value);
|
|
100
|
+
case "<":
|
|
101
|
+
return encodeURIComponent("<" + value);
|
|
102
|
+
case "<=":
|
|
103
|
+
return encodeURIComponent("<=" + value);
|
|
104
|
+
case "like":
|
|
105
|
+
return encodeURIComponent("%" + value + "%");
|
|
106
|
+
case "not like":
|
|
107
|
+
return encodeURIComponent("!%" + value + "%");
|
|
108
|
+
case "in":
|
|
109
|
+
return encodeURIComponent("in(" + value + ")");
|
|
110
|
+
case "not in":
|
|
111
|
+
return encodeURIComponent("!in(" + value + ")");
|
|
112
|
+
default:
|
|
113
|
+
return encodeURIComponent(value);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// === Utility: Inline nextSortState (matches db-manager/utils/sort-state.js) ===
|
|
118
|
+
function nextSortState(currentColumn, currentDir, clickedColumn) {
|
|
119
|
+
// Clicking a different column → start ascending on new column
|
|
120
|
+
if (clickedColumn !== currentColumn) {
|
|
121
|
+
return { column: clickedColumn, dir: "asc" };
|
|
122
|
+
}
|
|
123
|
+
// Same column clicked — cycle through states
|
|
124
|
+
if (currentDir === null || currentDir === undefined) {
|
|
125
|
+
return { column: clickedColumn, dir: "asc" };
|
|
126
|
+
}
|
|
127
|
+
if (currentDir === "asc") {
|
|
128
|
+
return { column: clickedColumn, dir: "desc" };
|
|
129
|
+
}
|
|
130
|
+
// currentDir === 'desc' → clear sort
|
|
131
|
+
return { column: null, dir: null };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// === Toast Messages ===
|
|
135
|
+
function showToast(message, type) {
|
|
136
|
+
var container = document.querySelector(".toast-container");
|
|
137
|
+
if (!container) {
|
|
138
|
+
container = document.createElement("div");
|
|
139
|
+
container.className = "toast-container";
|
|
140
|
+
document.body.appendChild(container);
|
|
141
|
+
}
|
|
142
|
+
var toast = document.createElement("div");
|
|
143
|
+
toast.className = "toast toast-" + (type || "success");
|
|
144
|
+
toast.textContent = message;
|
|
145
|
+
container.appendChild(toast);
|
|
146
|
+
setTimeout(function () {
|
|
147
|
+
if (toast.parentNode) {
|
|
148
|
+
toast.parentNode.removeChild(toast);
|
|
149
|
+
}
|
|
150
|
+
}, 3000);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// === API Helpers ===
|
|
154
|
+
function apiFetch(url, options) {
|
|
155
|
+
return fetch(url, options)
|
|
156
|
+
.then(function (res) {
|
|
157
|
+
if (!res.ok) {
|
|
158
|
+
return res.json().then(function (body) {
|
|
159
|
+
throw new Error(body.message || "Request failed");
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return res;
|
|
163
|
+
})
|
|
164
|
+
.catch(function (err) {
|
|
165
|
+
showToast(err.message || "Network error", "error");
|
|
166
|
+
throw err;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function apiGet(url) {
|
|
171
|
+
return apiFetch(url).then(function (res) {
|
|
172
|
+
return res.json();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function apiPost(url, body) {
|
|
177
|
+
return apiFetch(url, {
|
|
178
|
+
method: "POST",
|
|
179
|
+
headers: { "Content-Type": "application/json" },
|
|
180
|
+
body: JSON.stringify(body),
|
|
181
|
+
}).then(function (res) {
|
|
182
|
+
return res.json();
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function apiPut(url, body) {
|
|
187
|
+
return apiFetch(url, {
|
|
188
|
+
method: "PUT",
|
|
189
|
+
headers: { "Content-Type": "application/json" },
|
|
190
|
+
body: JSON.stringify(body),
|
|
191
|
+
}).then(function (res) {
|
|
192
|
+
return res.json();
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function apiDelete(url, body) {
|
|
197
|
+
return apiFetch(url, {
|
|
198
|
+
method: "DELETE",
|
|
199
|
+
headers: { "Content-Type": "application/json" },
|
|
200
|
+
body: JSON.stringify(body),
|
|
201
|
+
}).then(function (res) {
|
|
202
|
+
return res.json();
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// === Sidebar: Fetch and Render Tables ===
|
|
207
|
+
function fetchTables() {
|
|
208
|
+
apiGet("/api/tables").then(function (data) {
|
|
209
|
+
state.tables = data.tables || [];
|
|
210
|
+
renderTableList(state.tables);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function renderTableList(tables) {
|
|
215
|
+
if (!tableList) return;
|
|
216
|
+
tableList.innerHTML = "";
|
|
217
|
+
var isTablesPage = window.location.pathname === "/tables";
|
|
218
|
+
tables.forEach(function (name) {
|
|
219
|
+
var li = document.createElement("li");
|
|
220
|
+
li.textContent = name;
|
|
221
|
+
li.setAttribute("data-table", name);
|
|
222
|
+
if (name === state.activeTable) {
|
|
223
|
+
li.classList.add("active");
|
|
224
|
+
}
|
|
225
|
+
li.addEventListener("click", function () {
|
|
226
|
+
if (isTablesPage) {
|
|
227
|
+
selectTable(name);
|
|
228
|
+
} else {
|
|
229
|
+
window.location.href = "/tables?table=" + encodeURIComponent(name);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
tableList.appendChild(li);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// === Table Search ===
|
|
237
|
+
function onTableSearch() {
|
|
238
|
+
var query = tableSearchInput.value;
|
|
239
|
+
var filtered = filterTables(state.tables, query);
|
|
240
|
+
renderTableList(filtered);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// === Accordion Toggle ===
|
|
244
|
+
function setupAccordion() {
|
|
245
|
+
// Setup sidebar tables toggle
|
|
246
|
+
var tablesToggle = document.querySelector(".sidebar-tables-toggle");
|
|
247
|
+
if (tablesToggle) {
|
|
248
|
+
tablesToggle.addEventListener("click", function () {
|
|
249
|
+
var expanded = tablesToggle.getAttribute("aria-expanded") === "true";
|
|
250
|
+
tablesToggle.setAttribute("aria-expanded", expanded ? "false" : "true");
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// === History ===
|
|
256
|
+
function fetchHistory() {
|
|
257
|
+
apiGet("/api/history/connections").then(function (data) {
|
|
258
|
+
var connections = data.connections || [];
|
|
259
|
+
historyList.innerHTML = "";
|
|
260
|
+
connections.forEach(function (conn) {
|
|
261
|
+
var li = document.createElement("li");
|
|
262
|
+
li.textContent =
|
|
263
|
+
conn.db_type +
|
|
264
|
+
" — " +
|
|
265
|
+
conn.database_name +
|
|
266
|
+
" (" +
|
|
267
|
+
conn.connected_at +
|
|
268
|
+
")";
|
|
269
|
+
historyList.appendChild(li);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// === Table Selection ===
|
|
275
|
+
function selectTable(name) {
|
|
276
|
+
state.activeTable = name;
|
|
277
|
+
state.page = 0;
|
|
278
|
+
state.selectedKeys = [];
|
|
279
|
+
state.sortColumn = null;
|
|
280
|
+
state.sortDir = null;
|
|
281
|
+
state.filters = [];
|
|
282
|
+
updateSelectionButtons();
|
|
283
|
+
|
|
284
|
+
// Highlight active in sidebar
|
|
285
|
+
var items = tableList.querySelectorAll("li");
|
|
286
|
+
for (var i = 0; i < items.length; i++) {
|
|
287
|
+
items[i].classList.toggle(
|
|
288
|
+
"active",
|
|
289
|
+
items[i].getAttribute("data-table") === name,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Fetch schema then rows
|
|
294
|
+
apiGet("/api/tables/" + encodeURIComponent(name) + "/schema").then(
|
|
295
|
+
function (schema) {
|
|
296
|
+
state.schema = schema;
|
|
297
|
+
state.pkColumn = schema.pk || null;
|
|
298
|
+
renderColumnHeaders();
|
|
299
|
+
renderFilterTags();
|
|
300
|
+
fetchRows();
|
|
301
|
+
},
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// === Render Column Headers (with sort support) ===
|
|
306
|
+
function renderColumnHeaders() {
|
|
307
|
+
columnHeaders.innerHTML = "";
|
|
308
|
+
// Checkbox header
|
|
309
|
+
var thCheck = document.createElement("th");
|
|
310
|
+
var selectAll = document.createElement("input");
|
|
311
|
+
selectAll.type = "checkbox";
|
|
312
|
+
selectAll.setAttribute("aria-label", "Select all rows");
|
|
313
|
+
selectAll.addEventListener("change", function () {
|
|
314
|
+
toggleSelectAll(selectAll.checked);
|
|
315
|
+
});
|
|
316
|
+
thCheck.appendChild(selectAll);
|
|
317
|
+
columnHeaders.appendChild(thCheck);
|
|
318
|
+
|
|
319
|
+
// Column headers from schema
|
|
320
|
+
if (state.schema && state.schema.columns) {
|
|
321
|
+
state.schema.columns.forEach(function (col) {
|
|
322
|
+
var th = document.createElement("th");
|
|
323
|
+
th.className = "sortable";
|
|
324
|
+
th.setAttribute("data-column", col.name);
|
|
325
|
+
th.textContent = col.name + " ";
|
|
326
|
+
|
|
327
|
+
// Add sort indicator
|
|
328
|
+
if (state.sortColumn === col.name && state.sortDir) {
|
|
329
|
+
var icon = document.createElement("span");
|
|
330
|
+
icon.className = "material-icons sort-icon";
|
|
331
|
+
icon.textContent =
|
|
332
|
+
state.sortDir === "asc" ? "arrow_upward" : "arrow_downward";
|
|
333
|
+
th.appendChild(icon);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Sort click handler
|
|
337
|
+
th.addEventListener("click", function () {
|
|
338
|
+
var result = nextSortState(state.sortColumn, state.sortDir, col.name);
|
|
339
|
+
state.sortColumn = result.column;
|
|
340
|
+
state.sortDir = result.dir;
|
|
341
|
+
state.page = 0;
|
|
342
|
+
renderColumnHeaders();
|
|
343
|
+
fetchRows();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
columnHeaders.appendChild(th);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Actions header
|
|
351
|
+
var thActions = document.createElement("th");
|
|
352
|
+
thActions.textContent = "Actions";
|
|
353
|
+
columnHeaders.appendChild(thActions);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// === Filter Popup and Tags ===
|
|
357
|
+
function openFilterModal() {
|
|
358
|
+
if (!filterModalOverlay || !filterColSelect) return;
|
|
359
|
+
// Populate column select with current schema columns
|
|
360
|
+
filterColSelect.innerHTML = '<option value="">Column...</option>';
|
|
361
|
+
if (state.schema && state.schema.columns) {
|
|
362
|
+
state.schema.columns.forEach(function (col) {
|
|
363
|
+
var opt = document.createElement("option");
|
|
364
|
+
opt.value = col.name;
|
|
365
|
+
opt.textContent = col.name;
|
|
366
|
+
filterColSelect.appendChild(opt);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (filterValInput) filterValInput.value = "";
|
|
370
|
+
filterModalOverlay.style.display = "flex";
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function closeFilterModal() {
|
|
374
|
+
if (filterModalOverlay) filterModalOverlay.style.display = "none";
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function addFilter() {
|
|
378
|
+
if (!filterColSelect || !filterOpSelect || !filterValInput) return;
|
|
379
|
+
var col = filterColSelect.value;
|
|
380
|
+
var op = filterOpSelect.value;
|
|
381
|
+
var val = filterValInput.value.trim();
|
|
382
|
+
if (!col) {
|
|
383
|
+
showToast("Select a column", "error");
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (!val) {
|
|
387
|
+
showToast("Enter a value", "error");
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
state.filters.push({ column: col, operator: op, value: val });
|
|
392
|
+
state.page = 0;
|
|
393
|
+
renderFilterTags();
|
|
394
|
+
fetchRows();
|
|
395
|
+
closeFilterModal();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function removeFilter(index) {
|
|
399
|
+
state.filters.splice(index, 1);
|
|
400
|
+
state.page = 0;
|
|
401
|
+
renderFilterTags();
|
|
402
|
+
fetchRows();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function renderFilterTags() {
|
|
406
|
+
if (!filterTagsContainer) return;
|
|
407
|
+
filterTagsContainer.innerHTML = "";
|
|
408
|
+
state.filters.forEach(function (f, idx) {
|
|
409
|
+
var tag = document.createElement("span");
|
|
410
|
+
tag.className = "filter-tag";
|
|
411
|
+
|
|
412
|
+
var text = document.createElement("span");
|
|
413
|
+
text.className = "filter-tag-text";
|
|
414
|
+
text.textContent = f.column + " " + f.operator + " " + f.value;
|
|
415
|
+
text.title = f.column + " " + f.operator + " " + f.value;
|
|
416
|
+
tag.appendChild(text);
|
|
417
|
+
|
|
418
|
+
var removeBtn = document.createElement("button");
|
|
419
|
+
removeBtn.className = "filter-tag-remove";
|
|
420
|
+
removeBtn.innerHTML = "×";
|
|
421
|
+
removeBtn.title = "Remove filter";
|
|
422
|
+
removeBtn.addEventListener("click", function () {
|
|
423
|
+
removeFilter(idx);
|
|
424
|
+
});
|
|
425
|
+
tag.appendChild(removeBtn);
|
|
426
|
+
|
|
427
|
+
filterTagsContainer.appendChild(tag);
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// === Fetch Rows (with filter and sort params) ===
|
|
432
|
+
function fetchRows() {
|
|
433
|
+
var url =
|
|
434
|
+
"/api/tables/" +
|
|
435
|
+
encodeURIComponent(state.activeTable) +
|
|
436
|
+
"/rows?page=" +
|
|
437
|
+
state.page +
|
|
438
|
+
"&limit=" +
|
|
439
|
+
state.limit;
|
|
440
|
+
|
|
441
|
+
// Append sort params
|
|
442
|
+
if (state.sortColumn && state.sortDir) {
|
|
443
|
+
url +=
|
|
444
|
+
"&sort=" +
|
|
445
|
+
encodeURIComponent(state.sortColumn) +
|
|
446
|
+
"&dir=" +
|
|
447
|
+
encodeURIComponent(state.sortDir);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Append filter params using library syntax: ?column=prefixValue
|
|
451
|
+
for (var i = 0; i < state.filters.length; i++) {
|
|
452
|
+
var f = state.filters[i];
|
|
453
|
+
var encodedValue = encodeFilterValue(f.operator, f.value);
|
|
454
|
+
url += "&" + encodeURIComponent(f.column) + "=" + encodedValue;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
apiGet(url).then(function (result) {
|
|
458
|
+
state.rows = result.data || [];
|
|
459
|
+
state.totalCount = result.count || 0;
|
|
460
|
+
renderRows();
|
|
461
|
+
updatePagination();
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// === Render Rows (with Material Icon action buttons) ===
|
|
466
|
+
function renderRows() {
|
|
467
|
+
dataRows.innerHTML = "";
|
|
468
|
+
if (!state.schema || !state.schema.columns) return;
|
|
469
|
+
|
|
470
|
+
state.rows.forEach(function (row) {
|
|
471
|
+
var tr = document.createElement("tr");
|
|
472
|
+
var pkValue = state.pkColumn ? row[state.pkColumn] : null;
|
|
473
|
+
|
|
474
|
+
// Checkbox cell
|
|
475
|
+
var tdCheck = document.createElement("td");
|
|
476
|
+
var checkbox = document.createElement("input");
|
|
477
|
+
checkbox.type = "checkbox";
|
|
478
|
+
checkbox.setAttribute("aria-label", "Select row");
|
|
479
|
+
checkbox.checked = state.selectedKeys.indexOf(pkValue) !== -1;
|
|
480
|
+
checkbox.addEventListener("change", function () {
|
|
481
|
+
toggleRowSelection(pkValue, checkbox.checked);
|
|
482
|
+
tr.classList.toggle("selected", checkbox.checked);
|
|
483
|
+
});
|
|
484
|
+
tdCheck.appendChild(checkbox);
|
|
485
|
+
tr.appendChild(tdCheck);
|
|
486
|
+
|
|
487
|
+
// Data cells
|
|
488
|
+
state.schema.columns.forEach(function (col) {
|
|
489
|
+
var td = document.createElement("td");
|
|
490
|
+
var val = row[col.name];
|
|
491
|
+
td.textContent = formatCellValue(val);
|
|
492
|
+
td.setAttribute("data-column", col.name);
|
|
493
|
+
tr.appendChild(td);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Actions cell with Material Icons
|
|
497
|
+
var tdActions = document.createElement("td");
|
|
498
|
+
tdActions.className = "row-actions";
|
|
499
|
+
|
|
500
|
+
var btnEdit = document.createElement("button");
|
|
501
|
+
btnEdit.className = "btn-edit";
|
|
502
|
+
btnEdit.setAttribute("aria-label", "Edit row");
|
|
503
|
+
var editIcon = document.createElement("span");
|
|
504
|
+
editIcon.className = "material-icons";
|
|
505
|
+
editIcon.textContent = "edit";
|
|
506
|
+
btnEdit.appendChild(editIcon);
|
|
507
|
+
btnEdit.addEventListener("click", function () {
|
|
508
|
+
startInlineEdit(tr, row);
|
|
509
|
+
});
|
|
510
|
+
tdActions.appendChild(btnEdit);
|
|
511
|
+
|
|
512
|
+
var btnRowDelete = document.createElement("button");
|
|
513
|
+
btnRowDelete.className = "btn-row-delete";
|
|
514
|
+
btnRowDelete.setAttribute("aria-label", "Delete row");
|
|
515
|
+
var deleteIcon = document.createElement("span");
|
|
516
|
+
deleteIcon.className = "material-icons";
|
|
517
|
+
deleteIcon.textContent = "delete";
|
|
518
|
+
btnRowDelete.appendChild(deleteIcon);
|
|
519
|
+
btnRowDelete.addEventListener("click", function () {
|
|
520
|
+
deleteSingleRow(pkValue);
|
|
521
|
+
});
|
|
522
|
+
tdActions.appendChild(btnRowDelete);
|
|
523
|
+
|
|
524
|
+
tr.appendChild(tdActions);
|
|
525
|
+
|
|
526
|
+
if (state.selectedKeys.indexOf(pkValue) !== -1) {
|
|
527
|
+
tr.classList.add("selected");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
dataRows.appendChild(tr);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// === Delete Single Row ===
|
|
535
|
+
function deleteSingleRow(pkValue) {
|
|
536
|
+
if (!state.pkColumn || pkValue === null || pkValue === undefined) return;
|
|
537
|
+
var confirmed = confirm("Are you sure you want to delete this row?");
|
|
538
|
+
if (!confirmed) return;
|
|
539
|
+
|
|
540
|
+
apiDelete(
|
|
541
|
+
"/api/tables/" + encodeURIComponent(state.activeTable) + "/rows",
|
|
542
|
+
{
|
|
543
|
+
keys: [pkValue],
|
|
544
|
+
pkColumn: state.pkColumn,
|
|
545
|
+
},
|
|
546
|
+
).then(function (result) {
|
|
547
|
+
showToast(result.message || "Row deleted", "success");
|
|
548
|
+
// Remove from selection if selected
|
|
549
|
+
var idx = state.selectedKeys.indexOf(pkValue);
|
|
550
|
+
if (idx !== -1) {
|
|
551
|
+
state.selectedKeys.splice(idx, 1);
|
|
552
|
+
}
|
|
553
|
+
updateSelectionButtons();
|
|
554
|
+
fetchRows();
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// === Row Selection ===
|
|
559
|
+
function toggleRowSelection(pkValue, selected) {
|
|
560
|
+
if (selected) {
|
|
561
|
+
if (state.selectedKeys.indexOf(pkValue) === -1) {
|
|
562
|
+
state.selectedKeys.push(pkValue);
|
|
563
|
+
}
|
|
564
|
+
} else {
|
|
565
|
+
var idx = state.selectedKeys.indexOf(pkValue);
|
|
566
|
+
if (idx !== -1) {
|
|
567
|
+
state.selectedKeys.splice(idx, 1);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
updateSelectionButtons();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function toggleSelectAll(checked) {
|
|
574
|
+
state.selectedKeys = [];
|
|
575
|
+
if (checked) {
|
|
576
|
+
state.rows.forEach(function (row) {
|
|
577
|
+
if (state.pkColumn && row[state.pkColumn] !== undefined) {
|
|
578
|
+
state.selectedKeys.push(row[state.pkColumn]);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
renderRows();
|
|
583
|
+
updateSelectionButtons();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function updateSelectionButtons() {
|
|
587
|
+
var hasSelection = state.selectedKeys.length > 0;
|
|
588
|
+
if (btnDelete) btnDelete.disabled = !hasSelection;
|
|
589
|
+
if (btnExport) btnExport.disabled = !hasSelection;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// === Pagination ===
|
|
593
|
+
function updatePagination() {
|
|
594
|
+
var totalPages =
|
|
595
|
+
state.limit > 0 ? Math.ceil(state.totalCount / state.limit) : 1;
|
|
596
|
+
var currentPage = state.page + 1;
|
|
597
|
+
pageInfo.textContent =
|
|
598
|
+
state.totalCount > 0
|
|
599
|
+
? "Page " +
|
|
600
|
+
currentPage +
|
|
601
|
+
" of " +
|
|
602
|
+
totalPages +
|
|
603
|
+
" (" +
|
|
604
|
+
state.totalCount +
|
|
605
|
+
" rows)"
|
|
606
|
+
: "No data";
|
|
607
|
+
btnPrev.disabled = state.page <= 0;
|
|
608
|
+
btnNext.disabled = state.limit === 0 || currentPage >= totalPages;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function goToPrevPage() {
|
|
612
|
+
if (state.page > 0) {
|
|
613
|
+
state.page--;
|
|
614
|
+
fetchRows();
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function goToNextPage() {
|
|
619
|
+
var totalPages =
|
|
620
|
+
state.limit > 0 ? Math.ceil(state.totalCount / state.limit) : 1;
|
|
621
|
+
if (state.page + 1 < totalPages) {
|
|
622
|
+
state.page++;
|
|
623
|
+
fetchRows();
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function onPageSizeChange() {
|
|
628
|
+
state.limit = parseInt(pageSize.value, 10);
|
|
629
|
+
state.page = 0;
|
|
630
|
+
if (state.activeTable) {
|
|
631
|
+
fetchRows();
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// === Add Row Form ===
|
|
636
|
+
function showAddForm() {
|
|
637
|
+
if (!state.schema || !state.activeTable) {
|
|
638
|
+
showToast("Select a table first", "error");
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Remove existing form if present
|
|
643
|
+
removeAddForm();
|
|
644
|
+
|
|
645
|
+
var panel = document.querySelector(".data-panel-content");
|
|
646
|
+
var form = document.createElement("div");
|
|
647
|
+
form.className = "row-form add-row-form";
|
|
648
|
+
|
|
649
|
+
var title = document.createElement("h3");
|
|
650
|
+
title.textContent = "Add Row to " + state.activeTable;
|
|
651
|
+
form.appendChild(title);
|
|
652
|
+
|
|
653
|
+
var grid = document.createElement("div");
|
|
654
|
+
grid.className = "form-grid";
|
|
655
|
+
|
|
656
|
+
state.schema.columns.forEach(function (col) {
|
|
657
|
+
// Skip auto-increment PK fields
|
|
658
|
+
if (
|
|
659
|
+
col.pk &&
|
|
660
|
+
col.type &&
|
|
661
|
+
col.type.toUpperCase().indexOf("INTEGER") !== -1
|
|
662
|
+
) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
var field = document.createElement("div");
|
|
666
|
+
field.className = "form-field";
|
|
667
|
+
|
|
668
|
+
var label = document.createElement("label");
|
|
669
|
+
label.textContent = col.name + (col.nullable ? "" : " *");
|
|
670
|
+
label.setAttribute("for", "add-" + col.name);
|
|
671
|
+
field.appendChild(label);
|
|
672
|
+
|
|
673
|
+
var input = document.createElement("input");
|
|
674
|
+
input.type = "text";
|
|
675
|
+
input.id = "add-" + col.name;
|
|
676
|
+
input.name = col.name;
|
|
677
|
+
input.placeholder = col.type || "";
|
|
678
|
+
if (col.default !== null && col.default !== undefined) {
|
|
679
|
+
input.placeholder += " (default: " + col.default + ")";
|
|
680
|
+
}
|
|
681
|
+
field.appendChild(input);
|
|
682
|
+
grid.appendChild(field);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
form.appendChild(grid);
|
|
686
|
+
|
|
687
|
+
var actions = document.createElement("div");
|
|
688
|
+
actions.className = "form-actions";
|
|
689
|
+
|
|
690
|
+
var btnSave = document.createElement("button");
|
|
691
|
+
btnSave.className = "btn btn-save";
|
|
692
|
+
btnSave.textContent = "Save";
|
|
693
|
+
btnSave.type = "button";
|
|
694
|
+
btnSave.addEventListener("click", function () {
|
|
695
|
+
submitAddForm(form);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
var btnCancel = document.createElement("button");
|
|
699
|
+
btnCancel.className = "btn btn-cancel";
|
|
700
|
+
btnCancel.textContent = "Cancel";
|
|
701
|
+
btnCancel.type = "button";
|
|
702
|
+
btnCancel.addEventListener("click", function () {
|
|
703
|
+
removeAddForm();
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
actions.appendChild(btnSave);
|
|
707
|
+
actions.appendChild(btnCancel);
|
|
708
|
+
form.appendChild(actions);
|
|
709
|
+
|
|
710
|
+
panel.insertBefore(form, panel.firstChild);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function removeAddForm() {
|
|
714
|
+
var existing = document.querySelector(".add-row-form");
|
|
715
|
+
if (existing) {
|
|
716
|
+
existing.parentNode.removeChild(existing);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function submitAddForm(form) {
|
|
721
|
+
var inputs = form.querySelectorAll(".form-grid input");
|
|
722
|
+
var data = {};
|
|
723
|
+
for (var i = 0; i < inputs.length; i++) {
|
|
724
|
+
var name = inputs[i].name;
|
|
725
|
+
var value = inputs[i].value;
|
|
726
|
+
if (value !== "") {
|
|
727
|
+
data[name] = value;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (Object.keys(data).length === 0) {
|
|
732
|
+
showToast("Please fill in at least one field", "error");
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
apiPost("/api/tables/" + encodeURIComponent(state.activeTable) + "/rows", {
|
|
737
|
+
data: data,
|
|
738
|
+
}).then(function (result) {
|
|
739
|
+
showToast(result.message || "Row added successfully", "success");
|
|
740
|
+
removeAddForm();
|
|
741
|
+
fetchRows();
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// === Inline Edit ===
|
|
746
|
+
function startInlineEdit(tr, row) {
|
|
747
|
+
if (!state.schema || !state.pkColumn) return;
|
|
748
|
+
|
|
749
|
+
// Get data cells (skip checkbox at index 0 and actions at end)
|
|
750
|
+
var cells = tr.querySelectorAll("td[data-column]");
|
|
751
|
+
var originalValues = {};
|
|
752
|
+
|
|
753
|
+
for (var i = 0; i < cells.length; i++) {
|
|
754
|
+
var col = cells[i].getAttribute("data-column");
|
|
755
|
+
originalValues[col] = cells[i].textContent;
|
|
756
|
+
|
|
757
|
+
// Don't make PK editable
|
|
758
|
+
if (col === state.pkColumn) continue;
|
|
759
|
+
|
|
760
|
+
var input = document.createElement("input");
|
|
761
|
+
input.type = "text";
|
|
762
|
+
input.value = cells[i].textContent;
|
|
763
|
+
input.setAttribute("data-column", col);
|
|
764
|
+
cells[i].textContent = "";
|
|
765
|
+
cells[i].appendChild(input);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Replace actions
|
|
769
|
+
var actionsCell = tr.querySelector(".row-actions");
|
|
770
|
+
actionsCell.innerHTML = "";
|
|
771
|
+
|
|
772
|
+
var btnSave = document.createElement("button");
|
|
773
|
+
btnSave.className = "btn-row-save";
|
|
774
|
+
btnSave.textContent = "Save";
|
|
775
|
+
btnSave.addEventListener("click", function () {
|
|
776
|
+
saveInlineEdit(tr, row, originalValues);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
var btnCancel = document.createElement("button");
|
|
780
|
+
btnCancel.className = "btn-row-cancel";
|
|
781
|
+
btnCancel.textContent = "Cancel";
|
|
782
|
+
btnCancel.addEventListener("click", function () {
|
|
783
|
+
cancelInlineEdit(tr, originalValues);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
actionsCell.appendChild(btnSave);
|
|
787
|
+
actionsCell.appendChild(btnCancel);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function saveInlineEdit(tr, row, originalValues) {
|
|
791
|
+
var inputs = tr.querySelectorAll("td[data-column] input");
|
|
792
|
+
var data = {};
|
|
793
|
+
|
|
794
|
+
// Include PK value
|
|
795
|
+
if (state.pkColumn) {
|
|
796
|
+
data[state.pkColumn] = row[state.pkColumn];
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
for (var i = 0; i < inputs.length; i++) {
|
|
800
|
+
var col = inputs[i].getAttribute("data-column");
|
|
801
|
+
data[col] = inputs[i].value;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Include unchanged columns from original
|
|
805
|
+
for (var key in originalValues) {
|
|
806
|
+
if (!(key in data)) {
|
|
807
|
+
data[key] = originalValues[key];
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
apiPut("/api/tables/" + encodeURIComponent(state.activeTable) + "/rows", {
|
|
812
|
+
data: data,
|
|
813
|
+
uniqueKeys: [state.pkColumn],
|
|
814
|
+
}).then(function (result) {
|
|
815
|
+
showToast(result.message || "Row updated successfully", "success");
|
|
816
|
+
fetchRows();
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function cancelInlineEdit(tr, originalValues) {
|
|
821
|
+
var cells = tr.querySelectorAll("td[data-column]");
|
|
822
|
+
for (var i = 0; i < cells.length; i++) {
|
|
823
|
+
var col = cells[i].getAttribute("data-column");
|
|
824
|
+
cells[i].innerHTML = "";
|
|
825
|
+
cells[i].textContent = originalValues[col] || "";
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Restore action buttons with Material Icons
|
|
829
|
+
var actionsCell = tr.querySelector(".row-actions");
|
|
830
|
+
actionsCell.innerHTML = "";
|
|
831
|
+
|
|
832
|
+
var btnEdit = document.createElement("button");
|
|
833
|
+
btnEdit.className = "btn-edit";
|
|
834
|
+
btnEdit.setAttribute("aria-label", "Edit row");
|
|
835
|
+
var editIcon = document.createElement("span");
|
|
836
|
+
editIcon.className = "material-icons";
|
|
837
|
+
editIcon.textContent = "edit";
|
|
838
|
+
btnEdit.appendChild(editIcon);
|
|
839
|
+
btnEdit.addEventListener("click", function () {
|
|
840
|
+
startInlineEdit(tr, originalValues);
|
|
841
|
+
});
|
|
842
|
+
actionsCell.appendChild(btnEdit);
|
|
843
|
+
|
|
844
|
+
var btnRowDelete = document.createElement("button");
|
|
845
|
+
btnRowDelete.className = "btn-row-delete";
|
|
846
|
+
btnRowDelete.setAttribute("aria-label", "Delete row");
|
|
847
|
+
var deleteIcon = document.createElement("span");
|
|
848
|
+
deleteIcon.className = "material-icons";
|
|
849
|
+
deleteIcon.textContent = "delete";
|
|
850
|
+
btnRowDelete.appendChild(deleteIcon);
|
|
851
|
+
actionsCell.appendChild(btnRowDelete);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// === Delete Selected Rows ===
|
|
855
|
+
function deleteSelectedRows() {
|
|
856
|
+
if (state.selectedKeys.length === 0 || !state.pkColumn) return;
|
|
857
|
+
|
|
858
|
+
var count = state.selectedKeys.length;
|
|
859
|
+
var confirmed = confirm(
|
|
860
|
+
"Are you sure you want to delete " + count + " row(s)?",
|
|
861
|
+
);
|
|
862
|
+
if (!confirmed) return;
|
|
863
|
+
|
|
864
|
+
apiDelete(
|
|
865
|
+
"/api/tables/" + encodeURIComponent(state.activeTable) + "/rows",
|
|
866
|
+
{
|
|
867
|
+
keys: state.selectedKeys,
|
|
868
|
+
pkColumn: state.pkColumn,
|
|
869
|
+
},
|
|
870
|
+
).then(function (result) {
|
|
871
|
+
showToast(result.message || count + " row(s) deleted", "success");
|
|
872
|
+
state.selectedKeys = [];
|
|
873
|
+
updateSelectionButtons();
|
|
874
|
+
fetchRows();
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// === Export Selected Rows (CSV with Content-Disposition) ===
|
|
879
|
+
function exportSelectedRows() {
|
|
880
|
+
if (state.selectedKeys.length === 0 || !state.pkColumn) return;
|
|
881
|
+
|
|
882
|
+
var url =
|
|
883
|
+
"/api/tables/" + encodeURIComponent(state.activeTable) + "/export";
|
|
884
|
+
|
|
885
|
+
fetch(url, {
|
|
886
|
+
method: "POST",
|
|
887
|
+
headers: { "Content-Type": "application/json" },
|
|
888
|
+
body: JSON.stringify({
|
|
889
|
+
keys: state.selectedKeys,
|
|
890
|
+
pkColumn: state.pkColumn,
|
|
891
|
+
}),
|
|
892
|
+
})
|
|
893
|
+
.then(function (res) {
|
|
894
|
+
if (!res.ok) {
|
|
895
|
+
return res.json().then(function (body) {
|
|
896
|
+
throw new Error(body.message || "Export failed");
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
// Parse filename from Content-Disposition header
|
|
900
|
+
var disposition = res.headers.get("Content-Disposition");
|
|
901
|
+
var filename = null;
|
|
902
|
+
if (disposition) {
|
|
903
|
+
var match = disposition.match(/filename="?([^";\s]+)"?/);
|
|
904
|
+
if (match) {
|
|
905
|
+
filename = match[1];
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (!filename) {
|
|
909
|
+
var timestamp = new Date()
|
|
910
|
+
.toISOString()
|
|
911
|
+
.replace(/[-:]/g, "")
|
|
912
|
+
.replace(/\.\d+Z$/, "");
|
|
913
|
+
filename = state.activeTable + "_" + timestamp + ".csv";
|
|
914
|
+
}
|
|
915
|
+
return res.blob().then(function (blob) {
|
|
916
|
+
return { blob: blob, filename: filename };
|
|
917
|
+
});
|
|
918
|
+
})
|
|
919
|
+
.then(function (result) {
|
|
920
|
+
var a = document.createElement("a");
|
|
921
|
+
a.href = URL.createObjectURL(result.blob);
|
|
922
|
+
a.download = result.filename;
|
|
923
|
+
document.body.appendChild(a);
|
|
924
|
+
a.click();
|
|
925
|
+
document.body.removeChild(a);
|
|
926
|
+
URL.revokeObjectURL(a.href);
|
|
927
|
+
showToast("Export downloaded", "success");
|
|
928
|
+
})
|
|
929
|
+
.catch(function (err) {
|
|
930
|
+
showToast(err.message || "Export failed", "error");
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// === Query Page Logic ===
|
|
935
|
+
function initQueryPage() {
|
|
936
|
+
var queryContent = document.querySelector(".query-content");
|
|
937
|
+
if (!queryContent) return;
|
|
938
|
+
|
|
939
|
+
var queryEditor = queryContent.querySelector(".query-editor");
|
|
940
|
+
var btnRun = queryContent.querySelector(".btn-run");
|
|
941
|
+
var btnExportQuery = queryContent.querySelector(".btn-export-query");
|
|
942
|
+
var queryError = queryContent.querySelector(".query-error");
|
|
943
|
+
var queryColumnHeaders = queryContent.querySelector(
|
|
944
|
+
".query-column-headers",
|
|
945
|
+
);
|
|
946
|
+
var queryDataRows = queryContent.querySelector(".query-data-rows");
|
|
947
|
+
|
|
948
|
+
var lastQuery = "";
|
|
949
|
+
|
|
950
|
+
function clearResults() {
|
|
951
|
+
if (queryColumnHeaders) queryColumnHeaders.innerHTML = "";
|
|
952
|
+
if (queryDataRows) queryDataRows.innerHTML = "";
|
|
953
|
+
if (queryError) {
|
|
954
|
+
queryError.style.display = "none";
|
|
955
|
+
queryError.textContent = "";
|
|
956
|
+
}
|
|
957
|
+
if (btnExportQuery) btnExportQuery.disabled = true;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function showQueryError(message) {
|
|
961
|
+
if (queryError) {
|
|
962
|
+
queryError.textContent = message;
|
|
963
|
+
queryError.style.display = "block";
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function renderQueryResults(columns, data) {
|
|
968
|
+
// Render column headers
|
|
969
|
+
if (queryColumnHeaders) {
|
|
970
|
+
queryColumnHeaders.innerHTML = "";
|
|
971
|
+
columns.forEach(function (col) {
|
|
972
|
+
var th = document.createElement("th");
|
|
973
|
+
th.textContent = col;
|
|
974
|
+
queryColumnHeaders.appendChild(th);
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Render data rows
|
|
979
|
+
if (queryDataRows) {
|
|
980
|
+
queryDataRows.innerHTML = "";
|
|
981
|
+
data.forEach(function (row) {
|
|
982
|
+
var tr = document.createElement("tr");
|
|
983
|
+
columns.forEach(function (col) {
|
|
984
|
+
var td = document.createElement("td");
|
|
985
|
+
var val = row[col];
|
|
986
|
+
td.textContent = formatCellValue(val);
|
|
987
|
+
tr.appendChild(td);
|
|
988
|
+
});
|
|
989
|
+
queryDataRows.appendChild(tr);
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (btnExportQuery) btnExportQuery.disabled = false;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Run button handler
|
|
997
|
+
if (btnRun) {
|
|
998
|
+
btnRun.addEventListener("click", function () {
|
|
999
|
+
var queryText = queryEditor ? queryEditor.value.trim() : "";
|
|
1000
|
+
if (!queryText) {
|
|
1001
|
+
showToast("Please enter a query", "error");
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
lastQuery = queryText;
|
|
1005
|
+
clearResults();
|
|
1006
|
+
|
|
1007
|
+
fetch("/api/query", {
|
|
1008
|
+
method: "POST",
|
|
1009
|
+
headers: { "Content-Type": "application/json" },
|
|
1010
|
+
body: JSON.stringify({ query: queryText }),
|
|
1011
|
+
})
|
|
1012
|
+
.then(function (res) {
|
|
1013
|
+
return res.json().then(function (body) {
|
|
1014
|
+
return { ok: res.ok, body: body };
|
|
1015
|
+
});
|
|
1016
|
+
})
|
|
1017
|
+
.then(function (result) {
|
|
1018
|
+
if (!result.ok || result.body.error) {
|
|
1019
|
+
showQueryError(result.body.message || "Query execution failed");
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
renderQueryResults(
|
|
1023
|
+
result.body.columns || [],
|
|
1024
|
+
result.body.data || [],
|
|
1025
|
+
);
|
|
1026
|
+
})
|
|
1027
|
+
.catch(function (err) {
|
|
1028
|
+
showQueryError(err.message || "Network error");
|
|
1029
|
+
});
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// Export button handler
|
|
1034
|
+
if (btnExportQuery) {
|
|
1035
|
+
btnExportQuery.addEventListener("click", function () {
|
|
1036
|
+
if (!lastQuery) return;
|
|
1037
|
+
|
|
1038
|
+
fetch("/api/query/export", {
|
|
1039
|
+
method: "POST",
|
|
1040
|
+
headers: { "Content-Type": "application/json" },
|
|
1041
|
+
body: JSON.stringify({ query: lastQuery }),
|
|
1042
|
+
})
|
|
1043
|
+
.then(function (res) {
|
|
1044
|
+
if (!res.ok) {
|
|
1045
|
+
return res.json().then(function (body) {
|
|
1046
|
+
throw new Error(body.message || "Export failed");
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
// Parse filename from Content-Disposition header
|
|
1050
|
+
var disposition = res.headers.get("Content-Disposition");
|
|
1051
|
+
var filename = null;
|
|
1052
|
+
if (disposition) {
|
|
1053
|
+
var match = disposition.match(/filename="?([^";\s]+)"?/);
|
|
1054
|
+
if (match) {
|
|
1055
|
+
filename = match[1];
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (!filename) {
|
|
1059
|
+
var timestamp = new Date()
|
|
1060
|
+
.toISOString()
|
|
1061
|
+
.replace(/[-:]/g, "")
|
|
1062
|
+
.replace(/\.\d+Z$/, "");
|
|
1063
|
+
filename = "export_" + timestamp + ".csv";
|
|
1064
|
+
}
|
|
1065
|
+
return res.blob().then(function (blob) {
|
|
1066
|
+
return { blob: blob, filename: filename };
|
|
1067
|
+
});
|
|
1068
|
+
})
|
|
1069
|
+
.then(function (result) {
|
|
1070
|
+
var a = document.createElement("a");
|
|
1071
|
+
a.href = URL.createObjectURL(result.blob);
|
|
1072
|
+
a.download = result.filename;
|
|
1073
|
+
document.body.appendChild(a);
|
|
1074
|
+
a.click();
|
|
1075
|
+
document.body.removeChild(a);
|
|
1076
|
+
URL.revokeObjectURL(a.href);
|
|
1077
|
+
showToast("Export downloaded", "success");
|
|
1078
|
+
})
|
|
1079
|
+
.catch(function (err) {
|
|
1080
|
+
showToast(err.message || "Export failed", "error");
|
|
1081
|
+
});
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// === Dashboard Page Logic ===
|
|
1087
|
+
function initDashboardPage() {
|
|
1088
|
+
var dashboardContent = document.querySelector(".dashboard-content");
|
|
1089
|
+
if (!dashboardContent) return;
|
|
1090
|
+
|
|
1091
|
+
var tableBody = dashboardContent.querySelector(".dashboard-table-body");
|
|
1092
|
+
if (!tableBody) return;
|
|
1093
|
+
|
|
1094
|
+
apiGet("/api/dashboard").then(function (data) {
|
|
1095
|
+
var tables = data.tables || [];
|
|
1096
|
+
tableBody.innerHTML = "";
|
|
1097
|
+
|
|
1098
|
+
if (tables.length === 0) {
|
|
1099
|
+
var tr = document.createElement("tr");
|
|
1100
|
+
var td = document.createElement("td");
|
|
1101
|
+
td.colSpan = 5;
|
|
1102
|
+
td.textContent = "No tables found.";
|
|
1103
|
+
td.style.textAlign = "center";
|
|
1104
|
+
td.style.padding = "24px";
|
|
1105
|
+
tr.appendChild(td);
|
|
1106
|
+
tableBody.appendChild(tr);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
tables.forEach(function (table) {
|
|
1111
|
+
var tr = document.createElement("tr");
|
|
1112
|
+
tr.className = "dashboard-row";
|
|
1113
|
+
tr.setAttribute("data-table", table.name);
|
|
1114
|
+
|
|
1115
|
+
var tdName = document.createElement("td");
|
|
1116
|
+
tdName.className = "dashboard-table-name";
|
|
1117
|
+
tdName.textContent = table.name;
|
|
1118
|
+
tr.appendChild(tdName);
|
|
1119
|
+
|
|
1120
|
+
var tdCols = document.createElement("td");
|
|
1121
|
+
tdCols.textContent = table.columnCount;
|
|
1122
|
+
tr.appendChild(tdCols);
|
|
1123
|
+
|
|
1124
|
+
var tdIndexes = document.createElement("td");
|
|
1125
|
+
tdIndexes.textContent =
|
|
1126
|
+
table.indexCount !== undefined ? table.indexCount : "—";
|
|
1127
|
+
tr.appendChild(tdIndexes);
|
|
1128
|
+
|
|
1129
|
+
var tdRows = document.createElement("td");
|
|
1130
|
+
tdRows.textContent = table.rowCount.toLocaleString();
|
|
1131
|
+
tr.appendChild(tdRows);
|
|
1132
|
+
|
|
1133
|
+
var tdSize = document.createElement("td");
|
|
1134
|
+
tdSize.textContent =
|
|
1135
|
+
table.sizeMB !== undefined ? table.sizeMB.toFixed(3) : "—";
|
|
1136
|
+
tr.appendChild(tdSize);
|
|
1137
|
+
|
|
1138
|
+
tr.addEventListener("click", function () {
|
|
1139
|
+
window.location.href =
|
|
1140
|
+
"/tables?table=" + encodeURIComponent(table.name);
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
tableBody.appendChild(tr);
|
|
1144
|
+
});
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// === Sidebar Nav Link Active State and Click Handling ===
|
|
1149
|
+
function initNavLinks() {
|
|
1150
|
+
var navLinks = document.querySelectorAll(".nav-link");
|
|
1151
|
+
var currentPath = window.location.pathname;
|
|
1152
|
+
|
|
1153
|
+
for (var i = 0; i < navLinks.length; i++) {
|
|
1154
|
+
var link = navLinks[i];
|
|
1155
|
+
var page = link.getAttribute("data-page");
|
|
1156
|
+
|
|
1157
|
+
// Set active state based on current URL
|
|
1158
|
+
if (
|
|
1159
|
+
(page === "dashboard" && currentPath === "/dashboard") ||
|
|
1160
|
+
(page === "tables" && currentPath === "/tables") ||
|
|
1161
|
+
(page === "query" && currentPath === "/query") ||
|
|
1162
|
+
(page === "history" && currentPath === "/history")
|
|
1163
|
+
) {
|
|
1164
|
+
link.classList.add("active");
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Click handling
|
|
1168
|
+
(function (navLink) {
|
|
1169
|
+
navLink.addEventListener("click", function (e) {
|
|
1170
|
+
e.preventDefault();
|
|
1171
|
+
var href = navLink.getAttribute("href");
|
|
1172
|
+
if (href) {
|
|
1173
|
+
window.location.href = href;
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
})(link);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// === History Page Logic ===
|
|
1181
|
+
function initHistoryPage() {
|
|
1182
|
+
var historyContent = document.querySelector(".history-content");
|
|
1183
|
+
if (!historyContent) return;
|
|
1184
|
+
|
|
1185
|
+
var tableBody = historyContent.querySelector(".history-table-body");
|
|
1186
|
+
if (!tableBody) return;
|
|
1187
|
+
|
|
1188
|
+
apiGet("/api/history/queries").then(function (data) {
|
|
1189
|
+
var queries = data.queries || [];
|
|
1190
|
+
tableBody.innerHTML = "";
|
|
1191
|
+
|
|
1192
|
+
if (queries.length === 0) {
|
|
1193
|
+
var tr = document.createElement("tr");
|
|
1194
|
+
var td = document.createElement("td");
|
|
1195
|
+
td.colSpan = 4;
|
|
1196
|
+
td.textContent = "No query history found.";
|
|
1197
|
+
td.style.textAlign = "center";
|
|
1198
|
+
td.style.padding = "24px";
|
|
1199
|
+
tr.appendChild(td);
|
|
1200
|
+
tableBody.appendChild(tr);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
queries.forEach(function (q, idx) {
|
|
1205
|
+
var tr = document.createElement("tr");
|
|
1206
|
+
|
|
1207
|
+
var tdNum = document.createElement("td");
|
|
1208
|
+
tdNum.textContent = idx + 1;
|
|
1209
|
+
tr.appendChild(tdNum);
|
|
1210
|
+
|
|
1211
|
+
var tdQuery = document.createElement("td");
|
|
1212
|
+
tdQuery.className = "history-query-text";
|
|
1213
|
+
tdQuery.textContent = q.query_text || q.queryText || "";
|
|
1214
|
+
tdQuery.title = q.query_text || q.queryText || "";
|
|
1215
|
+
tr.appendChild(tdQuery);
|
|
1216
|
+
|
|
1217
|
+
var tdRows = document.createElement("td");
|
|
1218
|
+
tdRows.textContent =
|
|
1219
|
+
q.row_count !== undefined
|
|
1220
|
+
? q.row_count
|
|
1221
|
+
: q.rowCount !== undefined
|
|
1222
|
+
? q.rowCount
|
|
1223
|
+
: "—";
|
|
1224
|
+
tr.appendChild(tdRows);
|
|
1225
|
+
|
|
1226
|
+
var tdTime = document.createElement("td");
|
|
1227
|
+
tdTime.textContent = formatCellValue(
|
|
1228
|
+
q.executed_at || q.executedAt || "—",
|
|
1229
|
+
);
|
|
1230
|
+
tr.appendChild(tdTime);
|
|
1231
|
+
|
|
1232
|
+
tableBody.appendChild(tr);
|
|
1233
|
+
});
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// === Event Bindings ===
|
|
1238
|
+
function bindEvents() {
|
|
1239
|
+
if (tableSearchInput) {
|
|
1240
|
+
tableSearchInput.addEventListener("input", onTableSearch);
|
|
1241
|
+
}
|
|
1242
|
+
if (btnAdd) btnAdd.addEventListener("click", showAddForm);
|
|
1243
|
+
if (btnDelete) btnDelete.addEventListener("click", deleteSelectedRows);
|
|
1244
|
+
if (btnExport) btnExport.addEventListener("click", exportSelectedRows);
|
|
1245
|
+
if (btnFilter) btnFilter.addEventListener("click", openFilterModal);
|
|
1246
|
+
if (btnFilterAdd) btnFilterAdd.addEventListener("click", addFilter);
|
|
1247
|
+
if (filterModalClose)
|
|
1248
|
+
filterModalClose.addEventListener("click", closeFilterModal);
|
|
1249
|
+
if (filterModalOverlay) {
|
|
1250
|
+
filterModalOverlay.addEventListener("click", function (e) {
|
|
1251
|
+
if (e.target === filterModalOverlay) closeFilterModal();
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
if (filterValInput) {
|
|
1255
|
+
filterValInput.addEventListener("keydown", function (e) {
|
|
1256
|
+
if (e.key === "Enter") addFilter();
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
if (btnPrev) btnPrev.addEventListener("click", goToPrevPage);
|
|
1260
|
+
if (btnNext) btnNext.addEventListener("click", goToNextPage);
|
|
1261
|
+
if (pageSize) pageSize.addEventListener("change", onPageSizeChange);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// === Theme Switcher ===
|
|
1265
|
+
function initThemeSwitcher() {
|
|
1266
|
+
var themeBtns = document.querySelectorAll(".theme-btn");
|
|
1267
|
+
if (!themeBtns.length) return;
|
|
1268
|
+
|
|
1269
|
+
var currentTheme = localStorage.getItem("dbm-theme") || "system";
|
|
1270
|
+
|
|
1271
|
+
function setActiveBtn(theme) {
|
|
1272
|
+
for (var i = 0; i < themeBtns.length; i++) {
|
|
1273
|
+
var btn = themeBtns[i];
|
|
1274
|
+
if (btn.getAttribute("data-theme-value") === theme) {
|
|
1275
|
+
btn.classList.add("active");
|
|
1276
|
+
btn.setAttribute("aria-checked", "true");
|
|
1277
|
+
} else {
|
|
1278
|
+
btn.classList.remove("active");
|
|
1279
|
+
btn.setAttribute("aria-checked", "false");
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function applyTheme(theme) {
|
|
1285
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
1286
|
+
localStorage.setItem("dbm-theme", theme);
|
|
1287
|
+
setActiveBtn(theme);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Set initial state
|
|
1291
|
+
setActiveBtn(currentTheme);
|
|
1292
|
+
|
|
1293
|
+
// Bind click handlers
|
|
1294
|
+
for (var i = 0; i < themeBtns.length; i++) {
|
|
1295
|
+
(function (btn) {
|
|
1296
|
+
btn.addEventListener("click", function () {
|
|
1297
|
+
var theme = btn.getAttribute("data-theme-value");
|
|
1298
|
+
applyTheme(theme);
|
|
1299
|
+
});
|
|
1300
|
+
})(themeBtns[i]);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// === Initialize ===
|
|
1305
|
+
function init() {
|
|
1306
|
+
// Initialize theme switcher on all pages
|
|
1307
|
+
initThemeSwitcher();
|
|
1308
|
+
|
|
1309
|
+
// Initialize nav links on all pages
|
|
1310
|
+
initNavLinks();
|
|
1311
|
+
|
|
1312
|
+
// Setup sidebar tables accordion toggle
|
|
1313
|
+
setupAccordion();
|
|
1314
|
+
|
|
1315
|
+
// Always fetch and display tables in the sidebar (visible on all pages)
|
|
1316
|
+
fetchTables();
|
|
1317
|
+
|
|
1318
|
+
// Setup table search on all pages
|
|
1319
|
+
if (tableSearchInput) {
|
|
1320
|
+
tableSearchInput.addEventListener("input", onTableSearch);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// Detect which page we're on and initialize accordingly
|
|
1324
|
+
var queryContent = document.querySelector(".query-content");
|
|
1325
|
+
var dashboardContent = document.querySelector(".dashboard-content");
|
|
1326
|
+
var historyContent = document.querySelector(".history-content");
|
|
1327
|
+
|
|
1328
|
+
if (queryContent) {
|
|
1329
|
+
// Query page
|
|
1330
|
+
initQueryPage();
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
if (dashboardContent) {
|
|
1335
|
+
// Dashboard page
|
|
1336
|
+
initDashboardPage();
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
if (historyContent) {
|
|
1341
|
+
// History page
|
|
1342
|
+
initHistoryPage();
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// Default: Table browser page
|
|
1347
|
+
bindEvents();
|
|
1348
|
+
|
|
1349
|
+
// Check if a table is specified in the URL
|
|
1350
|
+
var urlParams = new URLSearchParams(window.location.search);
|
|
1351
|
+
var tableParam = urlParams.get("table");
|
|
1352
|
+
if (tableParam) {
|
|
1353
|
+
// Wait for tables to load, then select the specified table
|
|
1354
|
+
apiGet("/api/tables").then(function (data) {
|
|
1355
|
+
state.tables = data.tables || [];
|
|
1356
|
+
renderTableList(state.tables);
|
|
1357
|
+
if (state.tables.indexOf(tableParam) !== -1) {
|
|
1358
|
+
selectTable(tableParam);
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Start the app when DOM is ready
|
|
1365
|
+
if (document.readyState === "loading") {
|
|
1366
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
1367
|
+
} else {
|
|
1368
|
+
init();
|
|
1369
|
+
}
|
|
1370
|
+
})();
|