pglens 1.1.0 → 2.0.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/CHANGELOG.md +17 -0
- package/README.md +38 -145
- package/bin/pglens +143 -7
- package/client/app.js +654 -128
- package/client/index.html +133 -35
- package/client/styles.css +671 -37
- package/package.json +2 -2
- package/src/db/connection.js +163 -27
- package/src/routes/api.js +139 -9
- package/src/server.js +72 -30
- package/pglens-1.1.0.tgz +0 -0
package/client/app.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Main client-side application for viewing PostgreSQL database tables.
|
|
5
5
|
* Features:
|
|
6
|
+
* - Multi-database connection support
|
|
6
7
|
* - Multi-tab table viewing
|
|
7
8
|
* - Client-side sorting and column management
|
|
8
9
|
* - Cursor-based pagination for large tables
|
|
@@ -11,13 +12,18 @@
|
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
// Application state
|
|
14
|
-
let
|
|
15
|
+
let connections = []; // Array of active connections: { id, name, connectionString, sslMode }
|
|
16
|
+
let activeConnectionId = null; // Currently active connection ID
|
|
17
|
+
let tabs = []; // Array of tab objects: { connectionId, tableName, page, totalCount, sortColumn, sortDirection, data, hiddenColumns, columnWidths, cursor, cursorHistory, hasPrimaryKey, isApproximate }
|
|
15
18
|
let activeTabIndex = -1; // Currently active tab index
|
|
16
|
-
let allTables = []; // All available tables from the database
|
|
19
|
+
let allTables = []; // All available tables from the current database connection
|
|
17
20
|
let searchQuery = ''; // Current search filter for tables
|
|
18
21
|
let currentTheme = 'system'; // Current theme: 'light', 'dark', or 'system'
|
|
22
|
+
|
|
23
|
+
// UI Elements
|
|
19
24
|
const sidebar = document.getElementById('sidebar');
|
|
20
|
-
const sidebarContent =
|
|
25
|
+
const sidebarContent = document.getElementById('sidebarContent');
|
|
26
|
+
const connectionsList = document.getElementById('connectionsList');
|
|
21
27
|
const tableCount = document.getElementById('tableCount');
|
|
22
28
|
const sidebarToggle = document.getElementById('sidebarToggle');
|
|
23
29
|
const sidebarSearch = document.getElementById('sidebarSearch');
|
|
@@ -27,6 +33,25 @@ const tabsContainer = document.getElementById('tabsContainer');
|
|
|
27
33
|
const tabsBar = document.getElementById('tabsBar');
|
|
28
34
|
const tableView = document.getElementById('tableView');
|
|
29
35
|
const pagination = document.getElementById('pagination');
|
|
36
|
+
const addConnectionButton = document.getElementById('addConnectionButton');
|
|
37
|
+
|
|
38
|
+
// Connection UI Elements
|
|
39
|
+
const connectionDialog = document.getElementById('connectionDialog');
|
|
40
|
+
const closeConnectionDialogButton = document.getElementById('closeConnectionDialog');
|
|
41
|
+
const connectionNameInput = document.getElementById('connectionName');
|
|
42
|
+
const connectionUrlInput = document.getElementById('connectionUrl');
|
|
43
|
+
const connectionTabs = document.querySelectorAll('.connection-type-tab');
|
|
44
|
+
const modeUrl = document.getElementById('modeUrl');
|
|
45
|
+
const modeParams = document.getElementById('modeParams');
|
|
46
|
+
const connHost = document.getElementById('connHost');
|
|
47
|
+
const connPort = document.getElementById('connPort');
|
|
48
|
+
const connDatabase = document.getElementById('connDatabase');
|
|
49
|
+
const connUser = document.getElementById('connUser');
|
|
50
|
+
const connPassword = document.getElementById('connPassword');
|
|
51
|
+
const sslModeSelect = document.getElementById('sslMode');
|
|
52
|
+
const connectButton = document.getElementById('connectButton');
|
|
53
|
+
const connectionError = document.getElementById('connectionError');
|
|
54
|
+
const loadingOverlay = document.getElementById('loadingOverlay');
|
|
30
55
|
|
|
31
56
|
/**
|
|
32
57
|
* Initialize the application when DOM is ready.
|
|
@@ -34,7 +59,39 @@ const pagination = document.getElementById('pagination');
|
|
|
34
59
|
*/
|
|
35
60
|
document.addEventListener('DOMContentLoaded', () => {
|
|
36
61
|
initTheme();
|
|
37
|
-
|
|
62
|
+
fetchConnections(); // Check status and load connections
|
|
63
|
+
|
|
64
|
+
// Connection Event Listeners
|
|
65
|
+
connectButton.addEventListener('click', handleConnect);
|
|
66
|
+
addConnectionButton.addEventListener('click', () => showConnectionDialog(true));
|
|
67
|
+
closeConnectionDialogButton.addEventListener('click', hideConnectionDialog);
|
|
68
|
+
|
|
69
|
+
connectionTabs.forEach(tab => {
|
|
70
|
+
tab.addEventListener('click', () => {
|
|
71
|
+
// Switch active tab
|
|
72
|
+
connectionTabs.forEach(t => t.classList.remove('active'));
|
|
73
|
+
tab.classList.add('active');
|
|
74
|
+
|
|
75
|
+
// Show content
|
|
76
|
+
const target = tab.dataset.target;
|
|
77
|
+
if (target === 'url') {
|
|
78
|
+
modeUrl.style.display = 'block';
|
|
79
|
+
modeParams.style.display = 'none';
|
|
80
|
+
connectionDialog.dataset.inputMode = 'url';
|
|
81
|
+
} else {
|
|
82
|
+
modeUrl.style.display = 'none';
|
|
83
|
+
modeParams.style.display = 'block';
|
|
84
|
+
connectionDialog.dataset.inputMode = 'params';
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Allow Enter key to submit connection form
|
|
90
|
+
connectionUrlInput.addEventListener('keypress', (e) => {
|
|
91
|
+
if (e.key === 'Enter') {
|
|
92
|
+
handleConnect();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
38
95
|
|
|
39
96
|
sidebarToggle.addEventListener('click', () => {
|
|
40
97
|
if (tabs.length > 0) {
|
|
@@ -129,7 +186,7 @@ function updateThemeIcon() {
|
|
|
129
186
|
}
|
|
130
187
|
|
|
131
188
|
function updateSidebarToggleState() {
|
|
132
|
-
if (tabs.length === 0) {
|
|
189
|
+
if (tabs.length === 0 && connections.length === 0) {
|
|
133
190
|
sidebarToggle.disabled = true;
|
|
134
191
|
sidebarToggle.classList.add('disabled');
|
|
135
192
|
sidebar.classList.remove('minimized');
|
|
@@ -140,13 +197,397 @@ function updateSidebarToggleState() {
|
|
|
140
197
|
}
|
|
141
198
|
|
|
142
199
|
/**
|
|
143
|
-
*
|
|
200
|
+
* Fetch active connections from API.
|
|
201
|
+
*/
|
|
202
|
+
async function fetchConnections() {
|
|
203
|
+
try {
|
|
204
|
+
const response = await fetch('/api/connections');
|
|
205
|
+
const data = await response.json();
|
|
206
|
+
|
|
207
|
+
connections = data.connections || [];
|
|
208
|
+
|
|
209
|
+
if (connections.length > 0) {
|
|
210
|
+
if (!activeConnectionId || !connections.find(c => c.id === activeConnectionId)) {
|
|
211
|
+
activeConnectionId = connections[0].id;
|
|
212
|
+
}
|
|
213
|
+
renderConnectionsList();
|
|
214
|
+
loadTables();
|
|
215
|
+
hideConnectionDialog();
|
|
216
|
+
} else {
|
|
217
|
+
showConnectionDialog(false);
|
|
218
|
+
}
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error('Failed to fetch connections:', error);
|
|
221
|
+
showConnectionDialog(false);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Render the list of active connections in the sidebar.
|
|
227
|
+
*/
|
|
228
|
+
function renderConnectionsList() {
|
|
229
|
+
connectionsList.innerHTML = '';
|
|
230
|
+
|
|
231
|
+
connections.forEach(conn => {
|
|
232
|
+
const li = document.createElement('li');
|
|
233
|
+
li.className = 'connection-item';
|
|
234
|
+
if (conn.id === activeConnectionId) {
|
|
235
|
+
li.classList.add('active');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const nameSpan = document.createElement('span');
|
|
239
|
+
nameSpan.className = 'connection-name';
|
|
240
|
+
nameSpan.textContent = conn.name;
|
|
241
|
+
nameSpan.title = 'Click to edit connection';
|
|
242
|
+
nameSpan.style.cursor = 'pointer';
|
|
243
|
+
nameSpan.addEventListener('click', (e) => {
|
|
244
|
+
e.stopPropagation();
|
|
245
|
+
handleConnectionEdit(conn);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Add click event for the li to switch connection if not clicking name or close
|
|
249
|
+
li.addEventListener('click', (e) => {
|
|
250
|
+
if (e.target !== nameSpan && e.target !== disconnectBtn) {
|
|
251
|
+
switchConnection(conn.id);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const disconnectBtn = document.createElement('button');
|
|
256
|
+
disconnectBtn.className = 'connection-disconnect';
|
|
257
|
+
disconnectBtn.innerHTML = '×';
|
|
258
|
+
disconnectBtn.title = 'Disconnect';
|
|
259
|
+
disconnectBtn.addEventListener('click', (e) => {
|
|
260
|
+
e.stopPropagation();
|
|
261
|
+
handleDisconnect(conn.id);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
li.appendChild(nameSpan);
|
|
265
|
+
li.appendChild(disconnectBtn);
|
|
266
|
+
connectionsList.appendChild(li);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Switch the active connection.
|
|
272
|
+
* @param {string} connectionId - The connection ID to switch to
|
|
273
|
+
*/
|
|
274
|
+
function switchConnection(connectionId) {
|
|
275
|
+
if (activeConnectionId === connectionId) return;
|
|
276
|
+
|
|
277
|
+
activeConnectionId = connectionId;
|
|
278
|
+
renderConnectionsList();
|
|
279
|
+
loadTables();
|
|
280
|
+
|
|
281
|
+
// Clear tables view if no tab from this connection is active
|
|
282
|
+
const currentTab = getActiveTab();
|
|
283
|
+
if (currentTab && currentTab.connectionId !== activeConnectionId) {
|
|
284
|
+
// Try to find the last active tab for this connection
|
|
285
|
+
const tabIndex = tabs.findIndex(t => t.connectionId === activeConnectionId);
|
|
286
|
+
if (tabIndex !== -1) {
|
|
287
|
+
switchToTab(tabIndex);
|
|
288
|
+
} else {
|
|
289
|
+
// No tabs for this connection, show empty state
|
|
290
|
+
tableView.innerHTML = '<div class="empty-state"><p>Select a table from the sidebar to view its data</p></div>';
|
|
291
|
+
pagination.style.display = 'none';
|
|
292
|
+
|
|
293
|
+
// Deselect all tabs visually
|
|
294
|
+
const tabElements = tabsBar.querySelectorAll('.tab');
|
|
295
|
+
tabElements.forEach(el => el.classList.remove('active'));
|
|
296
|
+
activeTabIndex = -1;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Handle database connection.
|
|
303
|
+
*/
|
|
304
|
+
async function handleConnect() {
|
|
305
|
+
let url = '';
|
|
306
|
+
const connectionName = connectionNameInput.value.trim();
|
|
307
|
+
const sslMode = sslModeSelect.value;
|
|
308
|
+
const inputMode = connectionDialog.dataset.inputMode || 'url';
|
|
309
|
+
|
|
310
|
+
if (inputMode === 'url') {
|
|
311
|
+
url = connectionUrlInput.value.trim();
|
|
312
|
+
} else {
|
|
313
|
+
// Build URL from params
|
|
314
|
+
const host = connHost.value.trim() || 'localhost';
|
|
315
|
+
const port = connPort.value.trim() || '5432';
|
|
316
|
+
const database = connDatabase.value.trim() || 'postgres';
|
|
317
|
+
const user = connUser.value.trim();
|
|
318
|
+
const password = connPassword.value;
|
|
319
|
+
|
|
320
|
+
if (!user) {
|
|
321
|
+
showConnectionError('Username is required');
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
url = buildConnectionString(user, password, host, port, database);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (!url) {
|
|
329
|
+
showConnectionError('Please enter a connection URL');
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Validate URL format
|
|
334
|
+
try {
|
|
335
|
+
if (!url.startsWith('postgres://') && !url.startsWith('postgresql://')) {
|
|
336
|
+
showConnectionError('URL must start with postgres:// or postgresql://');
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const urlObj = new URL(url);
|
|
341
|
+
if (!urlObj.pathname || urlObj.pathname === '/') {
|
|
342
|
+
showConnectionError('URL must include a database name');
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
} catch (e) {
|
|
346
|
+
showConnectionError('Invalid URL format');
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
setConnectingState(true);
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
let urlPath = '/api/connect';
|
|
355
|
+
let method = 'POST';
|
|
356
|
+
|
|
357
|
+
if (connectionDialog.dataset.mode === 'edit') {
|
|
358
|
+
const id = connectionDialog.dataset.connectionId;
|
|
359
|
+
urlPath = `/api/connections/${id}`;
|
|
360
|
+
method = 'PUT';
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const response = await fetch(urlPath, {
|
|
364
|
+
method: method,
|
|
365
|
+
headers: { 'Content-Type': 'application/json' },
|
|
366
|
+
body: JSON.stringify({ url, sslMode, name: connectionName || undefined })
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const data = await response.json();
|
|
370
|
+
|
|
371
|
+
if (response.ok) {
|
|
372
|
+
if (data.connected || data.updated) {
|
|
373
|
+
// If updated, update local array
|
|
374
|
+
if (data.updated) {
|
|
375
|
+
const index = connections.findIndex(c => c.id === data.connectionId);
|
|
376
|
+
if (index !== -1) {
|
|
377
|
+
connections[index] = {
|
|
378
|
+
...connections[index],
|
|
379
|
+
name: data.name,
|
|
380
|
+
connectionString: url,
|
|
381
|
+
sslMode: sslMode
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
// New connection
|
|
386
|
+
// Check if this connection ID already exists in our list (backend might return existing one)
|
|
387
|
+
const existingIndex = connections.findIndex(c => c.id === data.connectionId);
|
|
388
|
+
if (existingIndex === -1) {
|
|
389
|
+
connections.push({
|
|
390
|
+
id: data.connectionId,
|
|
391
|
+
name: data.name,
|
|
392
|
+
connectionString: url,
|
|
393
|
+
sslMode: sslMode
|
|
394
|
+
});
|
|
395
|
+
} else {
|
|
396
|
+
console.log('Connection already exists, switching to it');
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
activeConnectionId = data.connectionId;
|
|
401
|
+
|
|
402
|
+
renderConnectionsList();
|
|
403
|
+
loadTables();
|
|
404
|
+
hideConnectionDialog();
|
|
405
|
+
// Don't clear inputs here, will clear on show
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
showConnectionError(data.error || 'Failed to connect');
|
|
409
|
+
}
|
|
410
|
+
} catch (error) {
|
|
411
|
+
showConnectionError(error.message);
|
|
412
|
+
} finally {
|
|
413
|
+
setConnectingState(false);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Handle database disconnection.
|
|
419
|
+
* @param {string} connectionId - ID of connection to disconnect
|
|
420
|
+
*/
|
|
421
|
+
async function handleDisconnect(connectionId) {
|
|
422
|
+
if (!confirm('Are you sure you want to disconnect? Associated tabs will be closed.')) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const response = await fetch('/api/disconnect', {
|
|
428
|
+
method: 'POST',
|
|
429
|
+
headers: { 'Content-Type': 'application/json' },
|
|
430
|
+
body: JSON.stringify({ connectionId })
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
if (response.ok) {
|
|
434
|
+
// Remove from local state
|
|
435
|
+
connections = connections.filter(c => c.id !== connectionId);
|
|
436
|
+
|
|
437
|
+
// Close associated tabs
|
|
438
|
+
const tabsToRemove = [];
|
|
439
|
+
tabs.forEach((tab, index) => {
|
|
440
|
+
if (tab.connectionId === connectionId) {
|
|
441
|
+
tabsToRemove.push(index);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Remove tabs in reverse order to maintain indices
|
|
446
|
+
for (let i = tabsToRemove.length - 1; i >= 0; i--) {
|
|
447
|
+
closeTab(tabsToRemove[i]);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (connections.length === 0) {
|
|
451
|
+
activeConnectionId = null;
|
|
452
|
+
sidebarContent.innerHTML = '';
|
|
453
|
+
tableCount.textContent = '0';
|
|
454
|
+
renderConnectionsList();
|
|
455
|
+
showConnectionDialog(false);
|
|
456
|
+
} else {
|
|
457
|
+
if (activeConnectionId === connectionId) {
|
|
458
|
+
// Switch to another connection (the first one)
|
|
459
|
+
activeConnectionId = connections[0].id;
|
|
460
|
+
loadTables();
|
|
461
|
+
}
|
|
462
|
+
renderConnectionsList();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} catch (error) {
|
|
466
|
+
console.error('Failed to disconnect:', error);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
function showConnectionDialog(allowClose, editMode = false, connection = null) {
|
|
473
|
+
connectionDialog.style.display = 'flex';
|
|
474
|
+
connectionError.style.display = 'none';
|
|
475
|
+
|
|
476
|
+
const title = connectionDialog.querySelector('h2');
|
|
477
|
+
title.textContent = editMode ? 'Edit Connection' : 'Connect to Database';
|
|
478
|
+
connectButton.textContent = editMode ? 'Save' : 'Connect';
|
|
479
|
+
|
|
480
|
+
connectionDialog.dataset.mode = editMode ? 'edit' : 'add';
|
|
481
|
+
|
|
482
|
+
if (editMode && connection) {
|
|
483
|
+
connectionDialog.dataset.connectionId = connection.id;
|
|
484
|
+
connectionNameInput.value = connection.name || '';
|
|
485
|
+
sslModeSelect.value = connection.sslMode || 'prefer';
|
|
486
|
+
|
|
487
|
+
// Try to parse URL to populate params
|
|
488
|
+
const parsed = parseConnectionString(connection.connectionString);
|
|
489
|
+
if (parsed) {
|
|
490
|
+
connHost.value = parsed.host;
|
|
491
|
+
connPort.value = parsed.port;
|
|
492
|
+
connDatabase.value = parsed.database;
|
|
493
|
+
connUser.value = parsed.user;
|
|
494
|
+
connPassword.value = parsed.password;
|
|
495
|
+
}
|
|
496
|
+
connectionUrlInput.value = connection.connectionString || '';
|
|
497
|
+
} else {
|
|
498
|
+
delete connectionDialog.dataset.connectionId;
|
|
499
|
+
connectionUrlInput.value = '';
|
|
500
|
+
connectionNameInput.value = '';
|
|
501
|
+
sslModeSelect.value = 'prefer';
|
|
502
|
+
|
|
503
|
+
// Reset params
|
|
504
|
+
connHost.value = 'localhost';
|
|
505
|
+
connPort.value = '5432';
|
|
506
|
+
connDatabase.value = 'postgres';
|
|
507
|
+
connUser.value = '';
|
|
508
|
+
connPassword.value = '';
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Reset tabs to Params mode by default (since we swapped buttons, tab[0] is Params)
|
|
512
|
+
connectionTabs.forEach(t => t.classList.remove('active'));
|
|
513
|
+
connectionTabs[0].classList.add('active');
|
|
514
|
+
|
|
515
|
+
modeUrl.style.display = 'none';
|
|
516
|
+
modeParams.style.display = 'block';
|
|
517
|
+
connectionDialog.dataset.inputMode = 'params';
|
|
518
|
+
|
|
519
|
+
connHost.focus();
|
|
520
|
+
|
|
521
|
+
if (allowClose && connections.length > 0) {
|
|
522
|
+
closeConnectionDialogButton.style.display = 'block';
|
|
523
|
+
} else {
|
|
524
|
+
closeConnectionDialogButton.style.display = 'none';
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function buildConnectionString(user, password, host, port, database) {
|
|
529
|
+
let auth = user;
|
|
530
|
+
if (password) {
|
|
531
|
+
auth += `:${encodeURIComponent(password)}`;
|
|
532
|
+
}
|
|
533
|
+
return `postgresql://${auth}@${host}:${port}/${database}`;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function parseConnectionString(urlStr) {
|
|
537
|
+
try {
|
|
538
|
+
if (!urlStr) return null;
|
|
539
|
+
// Handle cases where protocol might be missing (though validation enforces it)
|
|
540
|
+
if (!urlStr.includes('://')) {
|
|
541
|
+
urlStr = 'postgresql://' + urlStr;
|
|
542
|
+
}
|
|
543
|
+
const url = new URL(urlStr);
|
|
544
|
+
return {
|
|
545
|
+
host: url.hostname || 'localhost',
|
|
546
|
+
port: url.port || '5432',
|
|
547
|
+
database: url.pathname.replace(/^\//, '') || 'postgres',
|
|
548
|
+
user: url.username || '',
|
|
549
|
+
password: url.password || '' // Note: URL decoding happens automatically for username/password properties? Verify.
|
|
550
|
+
// Actually URL properties are usually decoded. decodeURIComponent check might be needed if raw.
|
|
551
|
+
};
|
|
552
|
+
} catch (e) {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function handleConnectionEdit(connection) {
|
|
558
|
+
showConnectionDialog(true, true, connection);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function hideConnectionDialog() {
|
|
562
|
+
connectionDialog.style.display = 'none';
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function showConnectionError(message) {
|
|
566
|
+
connectionError.textContent = message;
|
|
567
|
+
connectionError.style.display = 'block';
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function setConnectingState(isConnecting) {
|
|
571
|
+
connectButton.disabled = isConnecting;
|
|
572
|
+
connectButton.textContent = isConnecting ? 'Connecting...' : 'Connect';
|
|
573
|
+
connectionNameInput.disabled = isConnecting;
|
|
574
|
+
connectionUrlInput.disabled = isConnecting;
|
|
575
|
+
sslModeSelect.disabled = isConnecting;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Load all tables from the active database via API.
|
|
144
580
|
* Fetches table list and updates the sidebar.
|
|
145
581
|
*/
|
|
146
582
|
async function loadTables() {
|
|
583
|
+
if (!activeConnectionId) return;
|
|
584
|
+
|
|
147
585
|
try {
|
|
148
586
|
sidebarContent.innerHTML = '<div class="loading">Loading tables...</div>';
|
|
149
|
-
|
|
587
|
+
|
|
588
|
+
const response = await fetch('/api/tables', {
|
|
589
|
+
headers: { 'x-connection-id': activeConnectionId }
|
|
590
|
+
});
|
|
150
591
|
const data = await response.json();
|
|
151
592
|
|
|
152
593
|
if (data.error) {
|
|
@@ -225,7 +666,11 @@ function escapeHtml(text) {
|
|
|
225
666
|
}
|
|
226
667
|
|
|
227
668
|
function updateSidebarActiveState() {
|
|
228
|
-
const
|
|
669
|
+
const activeTab = getActiveTab();
|
|
670
|
+
|
|
671
|
+
// Only highlight sidebar if active tab belongs to active connection
|
|
672
|
+
const activeTable = (activeTab && activeTab.connectionId === activeConnectionId) ? activeTab.tableName : null;
|
|
673
|
+
|
|
229
674
|
if (!activeTable) {
|
|
230
675
|
const tableItems = sidebarContent.querySelectorAll('.table-list li');
|
|
231
676
|
tableItems.forEach(item => item.classList.remove('active'));
|
|
@@ -252,12 +697,18 @@ function updateSidebarActiveState() {
|
|
|
252
697
|
* @param {string} tableName - Name of the table to open
|
|
253
698
|
*/
|
|
254
699
|
function handleTableSelect(tableName) {
|
|
255
|
-
|
|
700
|
+
if (!activeConnectionId) return;
|
|
701
|
+
|
|
702
|
+
// Check if tab already exists for this table AND connection
|
|
703
|
+
const existingTabIndex = tabs.findIndex(tab =>
|
|
704
|
+
tab.tableName === tableName && tab.connectionId === activeConnectionId
|
|
705
|
+
);
|
|
256
706
|
|
|
257
707
|
if (existingTabIndex !== -1) {
|
|
258
708
|
switchToTab(existingTabIndex);
|
|
259
709
|
} else {
|
|
260
710
|
const newTab = {
|
|
711
|
+
connectionId: activeConnectionId,
|
|
261
712
|
tableName: tableName,
|
|
262
713
|
page: 1,
|
|
263
714
|
totalCount: 0,
|
|
@@ -269,7 +720,8 @@ function handleTableSelect(tableName) {
|
|
|
269
720
|
cursor: null, // Cursor for cursor-based pagination
|
|
270
721
|
cursorHistory: [], // History for backward navigation
|
|
271
722
|
hasPrimaryKey: false, // Whether table has primary key
|
|
272
|
-
isApproximate: false // Whether count is approximate (for large tables)
|
|
723
|
+
isApproximate: false, // Whether count is approximate (for large tables)
|
|
724
|
+
limit: 100 // Rows per page
|
|
273
725
|
};
|
|
274
726
|
tabs.push(newTab);
|
|
275
727
|
activeTabIndex = tabs.length - 1;
|
|
@@ -287,14 +739,12 @@ function switchToTab(index) {
|
|
|
287
739
|
activeTabIndex = index;
|
|
288
740
|
const tab = tabs[activeTabIndex];
|
|
289
741
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
});
|
|
742
|
+
// Switch connection if tab belongs to different connection
|
|
743
|
+
if (tab.connectionId !== activeConnectionId) {
|
|
744
|
+
activeConnectionId = tab.connectionId;
|
|
745
|
+
renderConnectionsList();
|
|
746
|
+
loadTables();
|
|
747
|
+
}
|
|
298
748
|
|
|
299
749
|
renderTabs();
|
|
300
750
|
updateSidebarActiveState();
|
|
@@ -312,29 +762,41 @@ function closeTab(index, event) {
|
|
|
312
762
|
event.stopPropagation();
|
|
313
763
|
}
|
|
314
764
|
|
|
315
|
-
if (tabs.length
|
|
316
|
-
|
|
765
|
+
if (index < 0 || index >= tabs.length) return;
|
|
766
|
+
|
|
767
|
+
const tabWasActive = (index === activeTabIndex);
|
|
768
|
+
tabs.splice(index, 1);
|
|
769
|
+
|
|
770
|
+
if (tabs.length === 0) {
|
|
317
771
|
activeTabIndex = -1;
|
|
318
772
|
tabsContainer.style.display = 'none';
|
|
319
773
|
tableView.innerHTML = '<div class="empty-state"><p>Select a table from the sidebar to view its data</p></div>';
|
|
320
774
|
pagination.style.display = 'none';
|
|
321
|
-
|
|
322
|
-
const tableItems = sidebarContent.querySelectorAll('.table-list li');
|
|
323
|
-
tableItems.forEach(item => item.classList.remove('active'));
|
|
324
|
-
|
|
775
|
+
updateSidebarActiveState();
|
|
325
776
|
updateSidebarToggleState();
|
|
326
777
|
return;
|
|
327
778
|
}
|
|
328
779
|
|
|
329
|
-
|
|
330
|
-
|
|
780
|
+
// Adjust activeTabIndex
|
|
331
781
|
if (activeTabIndex >= index) {
|
|
332
782
|
activeTabIndex--;
|
|
333
|
-
|
|
783
|
+
}
|
|
784
|
+
if (activeTabIndex < 0) {
|
|
785
|
+
activeTabIndex = 0;
|
|
334
786
|
}
|
|
335
787
|
|
|
336
|
-
|
|
337
|
-
|
|
788
|
+
// If we closed the active tab, switch to the new active one
|
|
789
|
+
if (tabWasActive) {
|
|
790
|
+
// Ensure index is within bounds
|
|
791
|
+
if (activeTabIndex >= tabs.length) activeTabIndex = tabs.length - 1;
|
|
792
|
+
|
|
793
|
+
const newActiveTab = tabs[activeTabIndex];
|
|
794
|
+
// Switch connection if needed
|
|
795
|
+
if (newActiveTab && newActiveTab.connectionId !== activeConnectionId) {
|
|
796
|
+
activeConnectionId = newActiveTab.connectionId;
|
|
797
|
+
renderConnectionsList();
|
|
798
|
+
loadTables();
|
|
799
|
+
}
|
|
338
800
|
}
|
|
339
801
|
|
|
340
802
|
renderTabs();
|
|
@@ -347,9 +809,7 @@ function closeTab(index, event) {
|
|
|
347
809
|
} else {
|
|
348
810
|
loadTableData();
|
|
349
811
|
}
|
|
350
|
-
|
|
351
812
|
updateSidebarActiveState();
|
|
352
|
-
updateSidebarToggleState();
|
|
353
813
|
}
|
|
354
814
|
}
|
|
355
815
|
|
|
@@ -372,6 +832,14 @@ function renderTabs() {
|
|
|
372
832
|
tabs.forEach((tab, index) => {
|
|
373
833
|
const tabElement = document.createElement('div');
|
|
374
834
|
tabElement.className = `tab ${index === activeTabIndex ? 'active' : ''}`;
|
|
835
|
+
// Add connection indicator for tab if multiple connections exist
|
|
836
|
+
if (connections.length > 1) {
|
|
837
|
+
const conn = connections.find(c => c.id === tab.connectionId);
|
|
838
|
+
if (conn) {
|
|
839
|
+
tabElement.title = `${tab.tableName} (${conn.name})`;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
375
843
|
tabElement.addEventListener('click', () => switchToTab(index));
|
|
376
844
|
|
|
377
845
|
const tabLabel = document.createElement('span');
|
|
@@ -396,9 +864,7 @@ function closeAllTabs() {
|
|
|
396
864
|
tableView.innerHTML = '<div class="empty-state"><p>Select a table from the sidebar to view its data</p></div>';
|
|
397
865
|
pagination.style.display = 'none';
|
|
398
866
|
|
|
399
|
-
|
|
400
|
-
tableItems.forEach(item => item.classList.remove('active'));
|
|
401
|
-
|
|
867
|
+
updateSidebarActiveState();
|
|
402
868
|
updateSidebarToggleState();
|
|
403
869
|
}
|
|
404
870
|
|
|
@@ -413,7 +879,8 @@ function handleRefresh() {
|
|
|
413
879
|
|
|
414
880
|
tab.data = null;
|
|
415
881
|
|
|
416
|
-
const
|
|
882
|
+
const refreshButton = document.querySelector('#refreshButton');
|
|
883
|
+
const refreshIcon = refreshButton ? refreshButton.querySelector('.refresh-icon') : null;
|
|
417
884
|
if (refreshIcon) {
|
|
418
885
|
refreshIcon.classList.add('spinning');
|
|
419
886
|
setTimeout(() => {
|
|
@@ -455,16 +922,26 @@ async function loadTableData() {
|
|
|
455
922
|
if (!tab) return;
|
|
456
923
|
|
|
457
924
|
try {
|
|
458
|
-
|
|
925
|
+
showLoading();
|
|
926
|
+
// tableView.innerHTML = '<div class="loading-state"><p>Loading data from ' + tab.tableName + '...</p></div>';
|
|
459
927
|
|
|
460
928
|
// Build query with cursor-based pagination if available
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
929
|
+
if (!tab.limit) {
|
|
930
|
+
tab.limit = 100; // Default limit for existing tabs
|
|
931
|
+
}
|
|
932
|
+
let queryString = `page=${tab.page}&limit=${tab.limit}`;
|
|
933
|
+
if (tab.sortColumn) {
|
|
934
|
+
queryString += `&sortColumn=${encodeURIComponent(tab.sortColumn)}&sortDirection=${tab.sortDirection}`;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (tab.hasPrimaryKey && tab.cursor && tab.page > 1 && !tab.sortColumn) {
|
|
938
|
+
// Use cursor for forward navigation (only if using default sort)
|
|
464
939
|
queryString += `&cursor=${encodeURIComponent(tab.cursor)}`;
|
|
465
940
|
}
|
|
466
941
|
|
|
467
|
-
const response = await fetch(`/api/tables/${tab.tableName}?${queryString}
|
|
942
|
+
const response = await fetch(`/api/tables/${tab.tableName}?${queryString}`, {
|
|
943
|
+
headers: { 'x-connection-id': tab.connectionId }
|
|
944
|
+
});
|
|
468
945
|
const data = await response.json();
|
|
469
946
|
|
|
470
947
|
if (data.error) {
|
|
@@ -499,6 +976,8 @@ async function loadTableData() {
|
|
|
499
976
|
} catch (error) {
|
|
500
977
|
tableView.innerHTML = '<div class="error-state"><p>Error: ' + error.message + '</p></div>';
|
|
501
978
|
pagination.style.display = 'none';
|
|
979
|
+
} finally {
|
|
980
|
+
hideLoading();
|
|
502
981
|
}
|
|
503
982
|
}
|
|
504
983
|
|
|
@@ -508,27 +987,59 @@ function renderTable(data) {
|
|
|
508
987
|
|
|
509
988
|
const columns = Object.keys(data.rows[0] || {});
|
|
510
989
|
|
|
990
|
+
if (!tab.limit) {
|
|
991
|
+
tab.limit = 100; // Default limit for existing tabs
|
|
992
|
+
}
|
|
993
|
+
|
|
511
994
|
const tableHeader = document.createElement('div');
|
|
512
995
|
tableHeader.className = 'table-header';
|
|
996
|
+
const startRow = ((tab.page - 1) * tab.limit) + 1;
|
|
997
|
+
const endRow = Math.min(tab.page * tab.limit, tab.totalCount);
|
|
998
|
+
const totalRows = tab.totalCount;
|
|
999
|
+
|
|
513
1000
|
tableHeader.innerHTML = `
|
|
514
1001
|
<div class="table-header-left">
|
|
515
1002
|
<h2>${tab.tableName}</h2>
|
|
516
|
-
<
|
|
517
|
-
<
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
1003
|
+
<div class="table-header-actions">
|
|
1004
|
+
<button class="refresh-button" id="refreshButton" title="Refresh data">
|
|
1005
|
+
<svg class="refresh-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1006
|
+
<path d="M8 2V6M8 14V10M2 8H6M10 8H14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
1007
|
+
<path d="M2.5 5.5C3.1 4.2 4.1 3.2 5.4 2.6M13.5 10.5C12.9 11.8 11.9 12.8 10.6 13.4M5.5 2.5C4.2 3.1 3.2 4.1 2.6 5.4M10.5 13.5C11.8 12.9 12.8 11.9 13.4 10.6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
1008
|
+
</svg>
|
|
1009
|
+
<span class="refresh-text">Refresh</span>
|
|
522
1010
|
</button>
|
|
523
|
-
<div class="
|
|
524
|
-
<
|
|
525
|
-
|
|
1011
|
+
<div class="limit-selector">
|
|
1012
|
+
<select id="limitSelect" class="limit-select" title="Rows per page">
|
|
1013
|
+
<option value="25" ${tab.limit === 25 ? 'selected' : ''}>25 rows</option>
|
|
1014
|
+
<option value="50" ${tab.limit === 50 ? 'selected' : ''}>50 rows</option>
|
|
1015
|
+
<option value="100" ${tab.limit === 100 ? 'selected' : ''}>100 rows</option>
|
|
1016
|
+
<option value="200" ${tab.limit === 200 ? 'selected' : ''}>200 rows</option>
|
|
1017
|
+
<option value="500" ${tab.limit === 500 ? 'selected' : ''}>500 rows</option>
|
|
1018
|
+
</select>
|
|
1019
|
+
</div>
|
|
1020
|
+
<div class="column-selector">
|
|
1021
|
+
<button class="column-button" id="columnButton" title="Show/Hide columns">
|
|
1022
|
+
<svg class="column-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1023
|
+
<path d="M3 2H13C13.5523 2 14 2.44772 14 3V13C14 13.5523 13.5523 14 13 14H3C2.44772 14 2 13.5523 2 13V3C2 2.44772 2.44772 2 3 2Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
1024
|
+
<path d="M6 2V14M10 2V14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
1025
|
+
</svg>
|
|
1026
|
+
<span class="column-label" id="columnLabel">Columns</span>
|
|
1027
|
+
</button>
|
|
1028
|
+
<div class="column-menu" id="columnMenu" style="display: none;">
|
|
1029
|
+
<div class="column-menu-header">Columns</div>
|
|
1030
|
+
<div class="column-menu-options" id="columnMenuOptions"></div>
|
|
1031
|
+
</div>
|
|
526
1032
|
</div>
|
|
527
1033
|
</div>
|
|
528
1034
|
</div>
|
|
529
|
-
<
|
|
530
|
-
|
|
531
|
-
|
|
1035
|
+
<div class="row-info-container">
|
|
1036
|
+
<span class="row-info">
|
|
1037
|
+
<span class="row-info-range">${startRow.toLocaleString()}–${endRow.toLocaleString()}</span>
|
|
1038
|
+
<span class="row-info-separator">of</span>
|
|
1039
|
+
<span class="row-info-total">${totalRows.toLocaleString()}</span>
|
|
1040
|
+
${tab.isApproximate ? '<span class="row-info-approx">(approx.)</span>' : ''}
|
|
1041
|
+
</span>
|
|
1042
|
+
</div>
|
|
532
1043
|
`;
|
|
533
1044
|
|
|
534
1045
|
const refreshButton = tableHeader.querySelector('#refreshButton');
|
|
@@ -536,6 +1047,21 @@ function renderTable(data) {
|
|
|
536
1047
|
refreshButton.addEventListener('click', handleRefresh);
|
|
537
1048
|
}
|
|
538
1049
|
|
|
1050
|
+
const limitSelect = tableHeader.querySelector('#limitSelect');
|
|
1051
|
+
if (limitSelect) {
|
|
1052
|
+
limitSelect.addEventListener('change', (e) => {
|
|
1053
|
+
const newLimit = parseInt(e.target.value, 10);
|
|
1054
|
+
if (tab.limit !== newLimit) {
|
|
1055
|
+
tab.limit = newLimit;
|
|
1056
|
+
tab.page = 1; // Reset to first page when limit changes
|
|
1057
|
+
tab.cursor = null; // Reset cursor
|
|
1058
|
+
tab.cursorHistory = [];
|
|
1059
|
+
tab.data = null; // Clear cache
|
|
1060
|
+
loadTableData();
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
|
|
539
1065
|
if (!tab.hiddenColumns) {
|
|
540
1066
|
tab.hiddenColumns = [];
|
|
541
1067
|
}
|
|
@@ -674,9 +1200,10 @@ function renderTable(data) {
|
|
|
674
1200
|
table.appendChild(thead);
|
|
675
1201
|
|
|
676
1202
|
const tbody = document.createElement('tbody');
|
|
677
|
-
|
|
1203
|
+
// Server-side sorting, so rows are already sorted
|
|
1204
|
+
const rows = data.rows || [];
|
|
678
1205
|
|
|
679
|
-
|
|
1206
|
+
rows.forEach(row => {
|
|
680
1207
|
const tr = document.createElement('tr');
|
|
681
1208
|
visibleColumns.forEach(column => {
|
|
682
1209
|
const td = document.createElement('td');
|
|
@@ -696,7 +1223,7 @@ function renderTable(data) {
|
|
|
696
1223
|
: 'NULL';
|
|
697
1224
|
td.dataset.columnName = column;
|
|
698
1225
|
|
|
699
|
-
td.addEventListener('
|
|
1226
|
+
td.addEventListener('click', (e) => {
|
|
700
1227
|
e.stopPropagation();
|
|
701
1228
|
showCellContentPopup(column, value);
|
|
702
1229
|
});
|
|
@@ -971,9 +1498,11 @@ function handleSort(column) {
|
|
|
971
1498
|
tab.sortDirection = 'asc';
|
|
972
1499
|
}
|
|
973
1500
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1501
|
+
// Reload data from server with new sort
|
|
1502
|
+
tab.page = 1; // Reset to page 1 on sort change
|
|
1503
|
+
tab.cursor = null;
|
|
1504
|
+
tab.cursorHistory = [];
|
|
1505
|
+
loadTableData();
|
|
977
1506
|
}
|
|
978
1507
|
|
|
979
1508
|
/**
|
|
@@ -1036,7 +1565,10 @@ function renderPagination() {
|
|
|
1036
1565
|
const tab = getActiveTab();
|
|
1037
1566
|
if (!tab) return;
|
|
1038
1567
|
|
|
1039
|
-
|
|
1568
|
+
if (!tab.limit) {
|
|
1569
|
+
tab.limit = 100; // Default limit for existing tabs
|
|
1570
|
+
}
|
|
1571
|
+
const limit = tab.limit;
|
|
1040
1572
|
const totalPages = Math.ceil(tab.totalCount / limit);
|
|
1041
1573
|
|
|
1042
1574
|
if (totalPages <= 1) {
|
|
@@ -1049,24 +1581,41 @@ function renderPagination() {
|
|
|
1049
1581
|
const hasPrevious = tab.page > 1;
|
|
1050
1582
|
const hasNext = tab.page < totalPages;
|
|
1051
1583
|
|
|
1584
|
+
const startRow = ((tab.page - 1) * limit) + 1;
|
|
1585
|
+
const endRow = Math.min(tab.page * limit, tab.totalCount);
|
|
1586
|
+
|
|
1052
1587
|
pagination.innerHTML = `
|
|
1053
|
-
<
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1588
|
+
<div class="pagination-content">
|
|
1589
|
+
<button
|
|
1590
|
+
class="pagination-button pagination-button-prev"
|
|
1591
|
+
${!hasPrevious ? 'disabled' : ''}
|
|
1592
|
+
onclick="handlePageChange(${tab.page - 1})"
|
|
1593
|
+
title="Previous page"
|
|
1594
|
+
>
|
|
1595
|
+
<svg class="pagination-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1596
|
+
<path d="M10 12L6 8L10 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
1597
|
+
</svg>
|
|
1598
|
+
<span>Previous</span>
|
|
1599
|
+
</button>
|
|
1600
|
+
<div class="pagination-info-container">
|
|
1601
|
+
<span class="pagination-info">
|
|
1602
|
+
<span class="pagination-page">Page <strong>${tab.page}</strong> of <strong>${totalPages}</strong></span>
|
|
1603
|
+
<span class="pagination-separator">•</span>
|
|
1604
|
+
<span class="pagination-rows">${startRow.toLocaleString()}–${endRow.toLocaleString()} of ${tab.totalCount.toLocaleString()}</span>
|
|
1605
|
+
</span>
|
|
1606
|
+
</div>
|
|
1607
|
+
<button
|
|
1608
|
+
class="pagination-button pagination-button-next"
|
|
1609
|
+
${!hasNext ? 'disabled' : ''}
|
|
1610
|
+
onclick="handlePageChange(${tab.page + 1})"
|
|
1611
|
+
title="Next page"
|
|
1612
|
+
>
|
|
1613
|
+
<span>Next</span>
|
|
1614
|
+
<svg class="pagination-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1615
|
+
<path d="M6 12L10 8L6 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
1616
|
+
</svg>
|
|
1617
|
+
</button>
|
|
1618
|
+
</div>
|
|
1070
1619
|
`;
|
|
1071
1620
|
}
|
|
1072
1621
|
|
|
@@ -1305,9 +1854,6 @@ function showCellContentPopup(column, value) {
|
|
|
1305
1854
|
const formattedContent = formatted.content;
|
|
1306
1855
|
const isJson = formatted.isJson;
|
|
1307
1856
|
const isDateTime = formatted.isDateTime;
|
|
1308
|
-
const dateValue = formatted.dateValue;
|
|
1309
|
-
|
|
1310
|
-
let currentTimezone = getCurrentTimezone();
|
|
1311
1857
|
|
|
1312
1858
|
const header = document.createElement('div');
|
|
1313
1859
|
header.className = 'cell-popup-header';
|
|
@@ -1320,66 +1866,41 @@ function showCellContentPopup(column, value) {
|
|
|
1320
1866
|
const headerActions = document.createElement('div');
|
|
1321
1867
|
headerActions.className = 'cell-popup-actions';
|
|
1322
1868
|
|
|
1323
|
-
let timezoneSelect = null;
|
|
1324
|
-
if (isDateTime && dateValue) {
|
|
1325
|
-
timezoneSelect = document.createElement('select');
|
|
1326
|
-
timezoneSelect.className = 'cell-popup-timezone';
|
|
1327
|
-
timezoneSelect.title = 'Select timezone';
|
|
1328
|
-
|
|
1329
|
-
const timezones = getCommonTimezones();
|
|
1330
|
-
timezones.forEach(tz => {
|
|
1331
|
-
const option = document.createElement('option');
|
|
1332
|
-
option.value = tz.value;
|
|
1333
|
-
option.textContent = tz.label;
|
|
1334
|
-
if (tz.value === currentTimezone) {
|
|
1335
|
-
option.selected = true;
|
|
1336
|
-
}
|
|
1337
|
-
timezoneSelect.appendChild(option);
|
|
1338
|
-
});
|
|
1339
|
-
|
|
1340
|
-
headerActions.appendChild(timezoneSelect);
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
1869
|
const copyButton = document.createElement('button');
|
|
1344
1870
|
copyButton.className = 'cell-popup-copy';
|
|
1345
1871
|
copyButton.innerHTML = '📋';
|
|
1346
1872
|
copyButton.title = 'Copy to clipboard';
|
|
1347
1873
|
|
|
1348
1874
|
const updateContent = () => {
|
|
1875
|
+
let contentToDisplay = '';
|
|
1876
|
+
let contentToCopy = '';
|
|
1877
|
+
|
|
1349
1878
|
if (value === null || value === undefined) {
|
|
1350
1879
|
content.classList.add('null-content');
|
|
1351
1880
|
content.classList.remove('json-value-popup', 'datetime-value-popup');
|
|
1352
|
-
|
|
1881
|
+
contentToDisplay = 'NULL';
|
|
1882
|
+
contentToCopy = 'NULL';
|
|
1353
1883
|
} else if (isJson) {
|
|
1354
|
-
const formatted = formatCellContentForPopup(value
|
|
1884
|
+
const formatted = formatCellContentForPopup(value);
|
|
1355
1885
|
content.classList.add('json-value-popup');
|
|
1356
1886
|
content.classList.remove('null-content', 'datetime-value-popup');
|
|
1357
|
-
|
|
1358
|
-
|
|
1887
|
+
contentToDisplay = formatted.content;
|
|
1888
|
+
contentToCopy = formatted.content;
|
|
1889
|
+
} else if (isDateTime) {
|
|
1890
|
+
// Display original date/time value without timezone conversion
|
|
1359
1891
|
content.classList.add('datetime-value-popup');
|
|
1360
1892
|
content.classList.remove('null-content', 'json-value-popup');
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
const localTz = formatDateTimeInTimezone(dateValue, getCurrentTimezone());
|
|
1364
|
-
const utcTz = formatDateTimeInTimezone(dateValue, 'UTC');
|
|
1365
|
-
const selectedTz = formatDateTimeInTimezone(dateValue, currentTimezone);
|
|
1366
|
-
|
|
1367
|
-
let displayText = `Local (${getCurrentTimezone()}): ${localTz}\n`;
|
|
1368
|
-
displayText += `UTC: ${utcTz}\n`;
|
|
1369
|
-
if (currentTimezone !== getCurrentTimezone() && currentTimezone !== 'UTC') {
|
|
1370
|
-
displayText += `Selected (${currentTimezone}): ${selectedTz}`;
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
content.textContent = displayText;
|
|
1893
|
+
contentToDisplay = String(value);
|
|
1894
|
+
contentToCopy = String(value);
|
|
1374
1895
|
} else {
|
|
1375
|
-
const formatted = formatCellContentForPopup(value
|
|
1896
|
+
const formatted = formatCellContentForPopup(value);
|
|
1376
1897
|
content.classList.remove('null-content', 'json-value-popup', 'datetime-value-popup');
|
|
1377
|
-
|
|
1898
|
+
contentToDisplay = formatted.content;
|
|
1899
|
+
contentToCopy = formatted.content;
|
|
1378
1900
|
}
|
|
1379
1901
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
copyButton._formattedContent = finalFormatted.content;
|
|
1902
|
+
content.textContent = contentToDisplay;
|
|
1903
|
+
copyButton._formattedContent = contentToCopy;
|
|
1383
1904
|
};
|
|
1384
1905
|
|
|
1385
1906
|
copyButton.addEventListener('click', async () => {
|
|
@@ -1424,13 +1945,6 @@ function showCellContentPopup(column, value) {
|
|
|
1424
1945
|
|
|
1425
1946
|
updateContent();
|
|
1426
1947
|
|
|
1427
|
-
if (timezoneSelect) {
|
|
1428
|
-
timezoneSelect.addEventListener('change', (e) => {
|
|
1429
|
-
currentTimezone = e.target.value;
|
|
1430
|
-
updateContent();
|
|
1431
|
-
});
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
1948
|
body.appendChild(content);
|
|
1435
1949
|
|
|
1436
1950
|
dialog.appendChild(header);
|
|
@@ -1462,3 +1976,15 @@ function closeCellContentPopup() {
|
|
|
1462
1976
|
overlay.remove();
|
|
1463
1977
|
}
|
|
1464
1978
|
}
|
|
1979
|
+
|
|
1980
|
+
function showLoading() {
|
|
1981
|
+
if (loadingOverlay) {
|
|
1982
|
+
loadingOverlay.style.display = 'flex';
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
function hideLoading() {
|
|
1987
|
+
if (loadingOverlay) {
|
|
1988
|
+
loadingOverlay.style.display = 'none';
|
|
1989
|
+
}
|
|
1990
|
+
}
|