pglens 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +6 -3
- package/client/app.js +589 -117
- package/client/index.html +26 -0
- package/client/styles.css +521 -18
- package/package.json +1 -1
package/client/app.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// Application state
|
|
15
15
|
let connections = []; // Array of active connections: { id, name, connectionString, sslMode }
|
|
16
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 }
|
|
17
|
+
let tabs = []; // Array of tab objects: { connectionId, tableName, page, totalCount, sortColumn, sortDirection, data, hiddenColumns, columnWidths, cursor, cursorHistory, hasPrimaryKey, isApproximate, abortController }
|
|
18
18
|
let activeTabIndex = -1; // Currently active tab index
|
|
19
19
|
let allTables = []; // All available tables from the current database connection
|
|
20
20
|
let searchQuery = ''; // Current search filter for tables
|
|
@@ -34,6 +34,13 @@ const tabsBar = document.getElementById('tabsBar');
|
|
|
34
34
|
const tableView = document.getElementById('tableView');
|
|
35
35
|
const pagination = document.getElementById('pagination');
|
|
36
36
|
const addConnectionButton = document.getElementById('addConnectionButton');
|
|
37
|
+
const landingPage = document.getElementById('landingPage');
|
|
38
|
+
const connectionsGrid = document.getElementById('connectionsGrid');
|
|
39
|
+
const connectionSearch = document.getElementById('connectionSearch');
|
|
40
|
+
const newConnectionBtn = document.getElementById('newConnectionBtn');
|
|
41
|
+
const sidebarTitle = document.querySelector('.sidebar-header-title');
|
|
42
|
+
const connectionsSection = document.querySelector('.connections-section');
|
|
43
|
+
const tablesSection = document.querySelector('.tables-section');
|
|
37
44
|
|
|
38
45
|
// Connection UI Elements
|
|
39
46
|
const connectionDialog = document.getElementById('connectionDialog');
|
|
@@ -59,11 +66,22 @@ const loadingOverlay = document.getElementById('loadingOverlay');
|
|
|
59
66
|
*/
|
|
60
67
|
document.addEventListener('DOMContentLoaded', () => {
|
|
61
68
|
initTheme();
|
|
69
|
+
|
|
70
|
+
if (sidebarTitle) {
|
|
71
|
+
sidebarTitle.style.cursor = 'pointer';
|
|
72
|
+
sidebarTitle.addEventListener('click', showLandingPage);
|
|
73
|
+
}
|
|
74
|
+
|
|
62
75
|
fetchConnections(); // Check status and load connections
|
|
63
76
|
|
|
64
77
|
// Connection Event Listeners
|
|
65
78
|
connectButton.addEventListener('click', handleConnect);
|
|
66
|
-
|
|
79
|
+
|
|
80
|
+
// Sidebar Add Button -> Redirect to Landing Page
|
|
81
|
+
addConnectionButton.addEventListener('click', () => {
|
|
82
|
+
showLandingPage();
|
|
83
|
+
});
|
|
84
|
+
|
|
67
85
|
closeConnectionDialogButton.addEventListener('click', hideConnectionDialog);
|
|
68
86
|
|
|
69
87
|
connectionTabs.forEach(tab => {
|
|
@@ -133,6 +151,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
133
151
|
applyTheme();
|
|
134
152
|
}
|
|
135
153
|
});
|
|
154
|
+
|
|
155
|
+
// Search Connections
|
|
156
|
+
if (connectionSearch) {
|
|
157
|
+
connectionSearch.addEventListener('input', () => {
|
|
158
|
+
renderLandingPage();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// New Connection Button
|
|
163
|
+
if (newConnectionBtn) {
|
|
164
|
+
newConnectionBtn.addEventListener('click', () => showConnectionDialog(true));
|
|
165
|
+
}
|
|
136
166
|
});
|
|
137
167
|
|
|
138
168
|
/**
|
|
@@ -206,15 +236,18 @@ async function fetchConnections() {
|
|
|
206
236
|
|
|
207
237
|
connections = data.connections || [];
|
|
208
238
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
239
|
+
renderConnectionsList();
|
|
240
|
+
hideConnectionDialog();
|
|
241
|
+
|
|
242
|
+
// If active connection is invalid (e.g. deleted), clear it
|
|
243
|
+
if (activeConnectionId && !connections.find(c => c.id === activeConnectionId)) {
|
|
244
|
+
activeConnectionId = null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (activeConnectionId) {
|
|
214
248
|
loadTables();
|
|
215
|
-
hideConnectionDialog();
|
|
216
249
|
} else {
|
|
217
|
-
|
|
250
|
+
showLandingPage();
|
|
218
251
|
}
|
|
219
252
|
} catch (error) {
|
|
220
253
|
console.error('Failed to fetch connections:', error);
|
|
@@ -222,10 +255,28 @@ async function fetchConnections() {
|
|
|
222
255
|
}
|
|
223
256
|
}
|
|
224
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Update sidebar visibility based on connections state.
|
|
260
|
+
*/
|
|
261
|
+
function updateSidebarVisibility() {
|
|
262
|
+
if (connections.length === 0) {
|
|
263
|
+
if (connectionsSection) connectionsSection.style.display = 'none';
|
|
264
|
+
if (tablesSection) tablesSection.style.display = 'none';
|
|
265
|
+
} else {
|
|
266
|
+
if (connectionsSection) connectionsSection.style.display = 'flex';
|
|
267
|
+
|
|
268
|
+
// Only show tables section if we have an active connection selected
|
|
269
|
+
if (tablesSection) {
|
|
270
|
+
tablesSection.style.display = activeConnectionId ? 'flex' : 'none';
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
225
275
|
/**
|
|
226
276
|
* Render the list of active connections in the sidebar.
|
|
227
277
|
*/
|
|
228
278
|
function renderConnectionsList() {
|
|
279
|
+
updateSidebarVisibility();
|
|
229
280
|
connectionsList.innerHTML = '';
|
|
230
281
|
|
|
231
282
|
connections.forEach(conn => {
|
|
@@ -238,31 +289,17 @@ function renderConnectionsList() {
|
|
|
238
289
|
const nameSpan = document.createElement('span');
|
|
239
290
|
nameSpan.className = 'connection-name';
|
|
240
291
|
nameSpan.textContent = conn.name;
|
|
241
|
-
nameSpan.title = '
|
|
242
|
-
nameSpan.style.cursor = 'pointer';
|
|
243
|
-
nameSpan.addEventListener('click', (e) => {
|
|
244
|
-
e.stopPropagation();
|
|
245
|
-
handleConnectionEdit(conn);
|
|
246
|
-
});
|
|
292
|
+
nameSpan.title = 'Switch to this connection';
|
|
247
293
|
|
|
248
|
-
//
|
|
249
|
-
li.addEventListener('click', (e) => {
|
|
250
|
-
if (e.target !== nameSpan && e.target !== disconnectBtn) {
|
|
251
|
-
switchConnection(conn.id);
|
|
252
|
-
}
|
|
253
|
-
});
|
|
294
|
+
// Actions removed as per user request. Management is done on Landing Page.
|
|
254
295
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
e.stopPropagation();
|
|
261
|
-
handleDisconnect(conn.id);
|
|
296
|
+
li.appendChild(nameSpan);
|
|
297
|
+
|
|
298
|
+
// Add click event for the whole li to switch connection
|
|
299
|
+
li.addEventListener('click', () => {
|
|
300
|
+
switchConnection(conn.id);
|
|
262
301
|
});
|
|
263
302
|
|
|
264
|
-
li.appendChild(nameSpan);
|
|
265
|
-
li.appendChild(disconnectBtn);
|
|
266
303
|
connectionsList.appendChild(li);
|
|
267
304
|
});
|
|
268
305
|
}
|
|
@@ -272,9 +309,24 @@ function renderConnectionsList() {
|
|
|
272
309
|
* @param {string} connectionId - The connection ID to switch to
|
|
273
310
|
*/
|
|
274
311
|
function switchConnection(connectionId) {
|
|
312
|
+
if (isAppBusy()) {
|
|
313
|
+
if (confirm('A query is currently loading. Do you want to cancel it and switch connections?')) {
|
|
314
|
+
cancelAllActiveRequests();
|
|
315
|
+
} else {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
275
319
|
if (activeConnectionId === connectionId) return;
|
|
276
320
|
|
|
277
321
|
activeConnectionId = connectionId;
|
|
322
|
+
|
|
323
|
+
// Hide landing page, show table view
|
|
324
|
+
landingPage.style.display = 'none';
|
|
325
|
+
tableView.style.display = 'flex';
|
|
326
|
+
if (tabs.length > 0) {
|
|
327
|
+
tabsContainer.style.display = 'block';
|
|
328
|
+
}
|
|
329
|
+
|
|
278
330
|
renderConnectionsList();
|
|
279
331
|
loadTables();
|
|
280
332
|
|
|
@@ -286,8 +338,8 @@ function switchConnection(connectionId) {
|
|
|
286
338
|
if (tabIndex !== -1) {
|
|
287
339
|
switchToTab(tabIndex);
|
|
288
340
|
} else {
|
|
289
|
-
// No tabs for this connection, show
|
|
290
|
-
|
|
341
|
+
// No tabs for this connection, show shimmer while loading
|
|
342
|
+
renderShimmerDashboard();
|
|
291
343
|
pagination.style.display = 'none';
|
|
292
344
|
|
|
293
345
|
// Deselect all tabs visually
|
|
@@ -397,11 +449,14 @@ async function handleConnect() {
|
|
|
397
449
|
}
|
|
398
450
|
}
|
|
399
451
|
|
|
400
|
-
|
|
452
|
+
// Don't auto-select the new connection. Show landing page.
|
|
453
|
+
activeConnectionId = null;
|
|
401
454
|
|
|
402
455
|
renderConnectionsList();
|
|
403
|
-
|
|
456
|
+
renderLandingPage(); // Update grid
|
|
404
457
|
hideConnectionDialog();
|
|
458
|
+
showLandingPage();
|
|
459
|
+
|
|
405
460
|
// Don't clear inputs here, will clear on show
|
|
406
461
|
}
|
|
407
462
|
} else {
|
|
@@ -452,7 +507,8 @@ async function handleDisconnect(connectionId) {
|
|
|
452
507
|
sidebarContent.innerHTML = '';
|
|
453
508
|
tableCount.textContent = '0';
|
|
454
509
|
renderConnectionsList();
|
|
455
|
-
|
|
510
|
+
renderLandingPage();
|
|
511
|
+
updateSidebarVisibility(); // Ensure sidebar hides
|
|
456
512
|
} else {
|
|
457
513
|
if (activeConnectionId === connectionId) {
|
|
458
514
|
// Switch to another connection (the first one)
|
|
@@ -460,6 +516,7 @@ async function handleDisconnect(connectionId) {
|
|
|
460
516
|
loadTables();
|
|
461
517
|
}
|
|
462
518
|
renderConnectionsList();
|
|
519
|
+
renderLandingPage(); // Update grid
|
|
463
520
|
}
|
|
464
521
|
}
|
|
465
522
|
} catch (error) {
|
|
@@ -469,6 +526,235 @@ async function handleDisconnect(connectionId) {
|
|
|
469
526
|
|
|
470
527
|
|
|
471
528
|
|
|
529
|
+
function showLandingPage() {
|
|
530
|
+
// Check if safe to navigate
|
|
531
|
+
if (isAppBusy()) { // Assuming isAppBusy and cancelAllActiveRequests are defined elsewhere
|
|
532
|
+
if (!confirm('A query is currently loading. Do you want to cancel it and go to the home page?')) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
cancelAllActiveRequests();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Hide other views
|
|
539
|
+
tableView.style.display = 'none';
|
|
540
|
+
tabsContainer.style.display = 'none';
|
|
541
|
+
pagination.style.display = 'none';
|
|
542
|
+
|
|
543
|
+
// Show landing page
|
|
544
|
+
landingPage.style.display = 'flex';
|
|
545
|
+
|
|
546
|
+
// clear active connection selection
|
|
547
|
+
activeConnectionId = null;
|
|
548
|
+
renderConnectionsList();
|
|
549
|
+
|
|
550
|
+
// Render grid
|
|
551
|
+
renderLandingPage();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function renderLandingPage() {
|
|
555
|
+
connectionsGrid.innerHTML = '';
|
|
556
|
+
|
|
557
|
+
const query = connectionSearch ? connectionSearch.value.toLowerCase().trim() : '';
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
if (!query) {
|
|
561
|
+
const addCard = document.createElement('div');
|
|
562
|
+
addCard.className = 'connection-card add-new-card';
|
|
563
|
+
addCard.innerHTML = `
|
|
564
|
+
<div class="add-new-icon-circle">
|
|
565
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
566
|
+
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
567
|
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
568
|
+
</svg>
|
|
569
|
+
</div>
|
|
570
|
+
<div class="add-new-text">Add Connection</div>
|
|
571
|
+
<div class="add-desc">Connect to a new database instance</div>
|
|
572
|
+
`;
|
|
573
|
+
addCard.addEventListener('click', () => {
|
|
574
|
+
showConnectionDialog(true);
|
|
575
|
+
});
|
|
576
|
+
connectionsGrid.appendChild(addCard);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Filter connections
|
|
580
|
+
const filteredConnections = connections.filter(conn => {
|
|
581
|
+
if (!query) return true;
|
|
582
|
+
const searchStr = `${conn.name} ${conn.connectionString}`.toLowerCase();
|
|
583
|
+
return searchStr.includes(query);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// Render existing connections
|
|
587
|
+
filteredConnections.forEach(conn => {
|
|
588
|
+
const card = document.createElement('div');
|
|
589
|
+
card.className = 'connection-card';
|
|
590
|
+
|
|
591
|
+
// Parse info
|
|
592
|
+
const parsed = parseConnectionString(conn.connectionString);
|
|
593
|
+
const hostDisplay = parsed ? parsed.host : 'localhost';
|
|
594
|
+
const portDisplay = parsed ? parsed.port : '5432';
|
|
595
|
+
const dbDisplay = parsed ? parsed.database : 'postgres';
|
|
596
|
+
const isSsl = conn.sslMode && conn.sslMode !== 'disable';
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
card.innerHTML = `
|
|
601
|
+
<div class="card-context-actions">
|
|
602
|
+
<button class="context-btn edit" title="Edit">✎</button>
|
|
603
|
+
<button class="context-btn delete" title="Disconnect">×</button>
|
|
604
|
+
</div>
|
|
605
|
+
|
|
606
|
+
<div class="card-top">
|
|
607
|
+
<div class="card-icon">
|
|
608
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
609
|
+
<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path>
|
|
610
|
+
<line x1="4" y1="22" x2="4" y2="15"></line>
|
|
611
|
+
</svg>
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
|
|
615
|
+
<div class="card-info">
|
|
616
|
+
<h3>${conn.name}</h3>
|
|
617
|
+
<p>PostgreSQL • ${dbDisplay}</p>
|
|
618
|
+
</div>
|
|
619
|
+
|
|
620
|
+
<div class="host-block">
|
|
621
|
+
<span class="host-label">HOST</span>
|
|
622
|
+
<div class="host-value">${hostDisplay}:${portDisplay}</div>
|
|
623
|
+
${isSsl ? '<span class="host-label" style="margin-top:4px;color:#10b981">SSL ENCRYPTED</span>' : ''}
|
|
624
|
+
</div>
|
|
625
|
+
|
|
626
|
+
<div class="card-actions-wrapper">
|
|
627
|
+
<button class="connect-btn">
|
|
628
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
629
|
+
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"></path>
|
|
630
|
+
</svg>
|
|
631
|
+
Connect
|
|
632
|
+
</button>
|
|
633
|
+
</div>
|
|
634
|
+
`;
|
|
635
|
+
|
|
636
|
+
// Click behavior
|
|
637
|
+
card.addEventListener('click', (e) => {
|
|
638
|
+
// Don't trigger if clicking actions
|
|
639
|
+
if (e.target.closest('.context-btn')) return;
|
|
640
|
+
switchConnection(conn.id);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// Connect Button
|
|
644
|
+
card.querySelector('.connect-btn').addEventListener('click', (e) => {
|
|
645
|
+
e.stopPropagation();
|
|
646
|
+
switchConnection(conn.id);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// Edit Action
|
|
650
|
+
const editBtn = card.querySelector('.edit');
|
|
651
|
+
editBtn.addEventListener('click', (e) => {
|
|
652
|
+
e.stopPropagation();
|
|
653
|
+
handleConnectionEdit(conn);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// Disconnect Action
|
|
657
|
+
const deleteBtn = card.querySelector('.delete');
|
|
658
|
+
deleteBtn.addEventListener('click', (e) => {
|
|
659
|
+
e.stopPropagation();
|
|
660
|
+
if (confirm(`Are you sure you want to disconnect from ${conn.name}?`)) {
|
|
661
|
+
handleDisconnect(conn.id);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
connectionsGrid.appendChild(card);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
if (filteredConnections.length === 0 && query) {
|
|
668
|
+
const noResults = document.createElement('div');
|
|
669
|
+
noResults.style.gridColumn = '1 / -1';
|
|
670
|
+
noResults.style.textAlign = 'center';
|
|
671
|
+
noResults.style.padding = '40px';
|
|
672
|
+
noResults.style.color = 'var(--text-secondary)';
|
|
673
|
+
noResults.innerHTML = `No connections found matching "${query}"`;
|
|
674
|
+
connectionsGrid.appendChild(noResults);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function renderTableDashboard() {
|
|
679
|
+
tableView.innerHTML = '';
|
|
680
|
+
|
|
681
|
+
const dashboard = document.createElement('div');
|
|
682
|
+
dashboard.className = 'table-dashboard';
|
|
683
|
+
|
|
684
|
+
const header = document.createElement('div');
|
|
685
|
+
header.className = 'table-dashboard-header';
|
|
686
|
+
header.innerHTML = `
|
|
687
|
+
<h2>Tables</h2>
|
|
688
|
+
<p>${allTables.length} tables available in this database.</p>
|
|
689
|
+
`;
|
|
690
|
+
dashboard.appendChild(header);
|
|
691
|
+
|
|
692
|
+
const grid = document.createElement('div');
|
|
693
|
+
grid.className = 'tables-grid';
|
|
694
|
+
|
|
695
|
+
allTables.forEach(table => {
|
|
696
|
+
const card = document.createElement('div');
|
|
697
|
+
card.className = 'table-card';
|
|
698
|
+
card.innerHTML = `
|
|
699
|
+
<div class="table-card-icon">
|
|
700
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
701
|
+
<line x1="3" y1="6" x2="21" y2="6"></line>
|
|
702
|
+
<line x1="3" y1="12" x2="21" y2="12"></line>
|
|
703
|
+
<line x1="3" y1="18" x2="21" y2="18"></line>
|
|
704
|
+
</svg>
|
|
705
|
+
</div>
|
|
706
|
+
<div class="table-card-info">
|
|
707
|
+
<div class="table-card-name" title="${table}">${table}</div>
|
|
708
|
+
<div class="table-card-schema">public</div>
|
|
709
|
+
</div>
|
|
710
|
+
`;
|
|
711
|
+
|
|
712
|
+
card.addEventListener('click', () => {
|
|
713
|
+
handleTableSelect(table);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
grid.appendChild(card);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
dashboard.appendChild(grid);
|
|
720
|
+
tableView.appendChild(dashboard);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function renderShimmerDashboard() {
|
|
724
|
+
tableView.innerHTML = '';
|
|
725
|
+
|
|
726
|
+
const dashboard = document.createElement('div');
|
|
727
|
+
dashboard.className = 'table-dashboard';
|
|
728
|
+
|
|
729
|
+
const header = document.createElement('div');
|
|
730
|
+
header.className = 'table-dashboard-header';
|
|
731
|
+
header.innerHTML = `
|
|
732
|
+
<h2>Tables</h2>
|
|
733
|
+
<div class="skeleton-text short" style="width: 200px; margin-top: 8px;"></div>
|
|
734
|
+
`;
|
|
735
|
+
dashboard.appendChild(header);
|
|
736
|
+
|
|
737
|
+
const grid = document.createElement('div');
|
|
738
|
+
grid.className = 'tables-grid';
|
|
739
|
+
|
|
740
|
+
// Render 16 skeleton cards
|
|
741
|
+
for (let i = 0; i < 16; i++) {
|
|
742
|
+
const card = document.createElement('div');
|
|
743
|
+
card.className = 'table-card skeleton-card';
|
|
744
|
+
card.innerHTML = `
|
|
745
|
+
<div class="skeleton-icon"></div>
|
|
746
|
+
<div class="table-card-info">
|
|
747
|
+
<div class="skeleton-text" style="width: ${60 + Math.random() * 30}%"></div>
|
|
748
|
+
<div class="skeleton-text short" style="width: 40%"></div>
|
|
749
|
+
</div>
|
|
750
|
+
`;
|
|
751
|
+
grid.appendChild(card);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
dashboard.appendChild(grid);
|
|
755
|
+
tableView.appendChild(dashboard);
|
|
756
|
+
}
|
|
757
|
+
|
|
472
758
|
function showConnectionDialog(allowClose, editMode = false, connection = null) {
|
|
473
759
|
connectionDialog.style.display = 'flex';
|
|
474
760
|
connectionError.style.display = 'none';
|
|
@@ -518,7 +804,7 @@ function showConnectionDialog(allowClose, editMode = false, connection = null) {
|
|
|
518
804
|
|
|
519
805
|
connHost.focus();
|
|
520
806
|
|
|
521
|
-
if (allowClose
|
|
807
|
+
if (allowClose) {
|
|
522
808
|
closeConnectionDialogButton.style.display = 'block';
|
|
523
809
|
} else {
|
|
524
810
|
closeConnectionDialogButton.style.display = 'none';
|
|
@@ -546,7 +832,7 @@ function parseConnectionString(urlStr) {
|
|
|
546
832
|
port: url.port || '5432',
|
|
547
833
|
database: url.pathname.replace(/^\//, '') || 'postgres',
|
|
548
834
|
user: url.username || '',
|
|
549
|
-
password: url.password || '' // Note: URL decoding happens automatically for username/password properties? Verify.
|
|
835
|
+
password: url.password || '' // Note: URL decoding happens automatically for username/password properties? Verify.
|
|
550
836
|
// Actually URL properties are usually decoded. decodeURIComponent check might be needed if raw.
|
|
551
837
|
};
|
|
552
838
|
} catch (e) {
|
|
@@ -585,6 +871,12 @@ async function loadTables() {
|
|
|
585
871
|
try {
|
|
586
872
|
sidebarContent.innerHTML = '<div class="loading">Loading tables...</div>';
|
|
587
873
|
|
|
874
|
+
// Only show shimmer if we aren't already looking at a table for this connection
|
|
875
|
+
const existingTab = getActiveTab();
|
|
876
|
+
if (!existingTab || existingTab.connectionId !== activeConnectionId) {
|
|
877
|
+
renderShimmerDashboard();
|
|
878
|
+
}
|
|
879
|
+
|
|
588
880
|
const response = await fetch('/api/tables', {
|
|
589
881
|
headers: { 'x-connection-id': activeConnectionId }
|
|
590
882
|
});
|
|
@@ -597,7 +889,19 @@ async function loadTables() {
|
|
|
597
889
|
allTables = data.tables;
|
|
598
890
|
tableCount.textContent = allTables.length;
|
|
599
891
|
|
|
892
|
+
// If no tables, show message
|
|
893
|
+
if (allTables.length === 0) {
|
|
894
|
+
tableView.innerHTML = '<div class="no-data-message">No tables found in this database</div>';
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
600
898
|
filterAndRenderTables();
|
|
899
|
+
|
|
900
|
+
// Only show dashboard if we aren't already looking at a table for this connection
|
|
901
|
+
const currentTab = getActiveTab();
|
|
902
|
+
if (!currentTab || currentTab.connectionId !== activeConnectionId) {
|
|
903
|
+
renderTableDashboard();
|
|
904
|
+
}
|
|
601
905
|
} catch (error) {
|
|
602
906
|
sidebarContent.innerHTML = `<div class="error">Error: ${error.message}</div>`;
|
|
603
907
|
}
|
|
@@ -699,6 +1003,14 @@ function updateSidebarActiveState() {
|
|
|
699
1003
|
function handleTableSelect(tableName) {
|
|
700
1004
|
if (!activeConnectionId) return;
|
|
701
1005
|
|
|
1006
|
+
if (isAppBusy()) {
|
|
1007
|
+
if (confirm('A query is currently loading. Do you want to cancel it and open this table?')) {
|
|
1008
|
+
cancelAllActiveRequests();
|
|
1009
|
+
} else {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
702
1014
|
// Check if tab already exists for this table AND connection
|
|
703
1015
|
const existingTabIndex = tabs.findIndex(tab =>
|
|
704
1016
|
tab.tableName === tableName && tab.connectionId === activeConnectionId
|
|
@@ -721,7 +1033,8 @@ function handleTableSelect(tableName) {
|
|
|
721
1033
|
cursorHistory: [], // History for backward navigation
|
|
722
1034
|
hasPrimaryKey: false, // Whether table has primary key
|
|
723
1035
|
isApproximate: false, // Whether count is approximate (for large tables)
|
|
724
|
-
limit: 100 // Rows per page
|
|
1036
|
+
limit: 100, // Rows per page
|
|
1037
|
+
abortController: null // Controller for cancelling requests
|
|
725
1038
|
};
|
|
726
1039
|
tabs.push(newTab);
|
|
727
1040
|
activeTabIndex = tabs.length - 1;
|
|
@@ -736,6 +1049,14 @@ function handleTableSelect(tableName) {
|
|
|
736
1049
|
function switchToTab(index) {
|
|
737
1050
|
if (index < 0 || index >= tabs.length) return;
|
|
738
1051
|
|
|
1052
|
+
if (isAppBusy() && index !== activeTabIndex) {
|
|
1053
|
+
if (confirm('A query is currently loading. Do you want to cancel it and switch tabs?')) {
|
|
1054
|
+
cancelAllActiveRequests();
|
|
1055
|
+
} else {
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
739
1060
|
activeTabIndex = index;
|
|
740
1061
|
const tab = tabs[activeTabIndex];
|
|
741
1062
|
|
|
@@ -770,8 +1091,14 @@ function closeTab(index, event) {
|
|
|
770
1091
|
if (tabs.length === 0) {
|
|
771
1092
|
activeTabIndex = -1;
|
|
772
1093
|
tabsContainer.style.display = 'none';
|
|
773
|
-
|
|
1094
|
+
if (activeConnectionId) {
|
|
1095
|
+
renderTableDashboard();
|
|
1096
|
+
} else {
|
|
1097
|
+
tableView.innerHTML = '';
|
|
1098
|
+
showLandingPage();
|
|
1099
|
+
}
|
|
774
1100
|
pagination.style.display = 'none';
|
|
1101
|
+
|
|
775
1102
|
updateSidebarActiveState();
|
|
776
1103
|
updateSidebarToggleState();
|
|
777
1104
|
return;
|
|
@@ -829,14 +1156,24 @@ function renderTabs() {
|
|
|
829
1156
|
closeAllButton.addEventListener('click', closeAllTabs);
|
|
830
1157
|
tabsBar.appendChild(closeAllButton);
|
|
831
1158
|
|
|
1159
|
+
// Check if we have tabs from multiple different connections
|
|
1160
|
+
const uniqueConnIds = new Set(tabs.map(t => t.connectionId));
|
|
1161
|
+
const showServerBadge = uniqueConnIds.size > 1;
|
|
1162
|
+
|
|
832
1163
|
tabs.forEach((tab, index) => {
|
|
833
1164
|
const tabElement = document.createElement('div');
|
|
834
1165
|
tabElement.className = `tab ${index === activeTabIndex ? 'active' : ''}`;
|
|
835
|
-
|
|
836
|
-
if
|
|
1166
|
+
|
|
1167
|
+
// Add connection indicator for tab if multiple connections exist in open tabs
|
|
1168
|
+
if (showServerBadge) {
|
|
837
1169
|
const conn = connections.find(c => c.id === tab.connectionId);
|
|
838
1170
|
if (conn) {
|
|
839
1171
|
tabElement.title = `${tab.tableName} (${conn.name})`;
|
|
1172
|
+
|
|
1173
|
+
const badge = document.createElement('span');
|
|
1174
|
+
badge.className = 'tab-server-badge';
|
|
1175
|
+
badge.textContent = conn.name;
|
|
1176
|
+
tabElement.appendChild(badge);
|
|
840
1177
|
}
|
|
841
1178
|
}
|
|
842
1179
|
|
|
@@ -861,7 +1198,7 @@ function closeAllTabs() {
|
|
|
861
1198
|
tabs = [];
|
|
862
1199
|
activeTabIndex = -1;
|
|
863
1200
|
tabsContainer.style.display = 'none';
|
|
864
|
-
|
|
1201
|
+
renderTableDashboard();
|
|
865
1202
|
pagination.style.display = 'none';
|
|
866
1203
|
|
|
867
1204
|
updateSidebarActiveState();
|
|
@@ -921,9 +1258,18 @@ async function loadTableData() {
|
|
|
921
1258
|
const tab = getActiveTab();
|
|
922
1259
|
if (!tab) return;
|
|
923
1260
|
|
|
1261
|
+
// Cancel any pending request for this tab
|
|
1262
|
+
if (tab.abortController) {
|
|
1263
|
+
tab.abortController.abort();
|
|
1264
|
+
tab.abortController = null;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
924
1267
|
try {
|
|
925
|
-
|
|
926
|
-
|
|
1268
|
+
|
|
1269
|
+
renderShimmerTable(tab);
|
|
1270
|
+
|
|
1271
|
+
tab.abortController = new AbortController();
|
|
1272
|
+
const signal = tab.abortController.signal;
|
|
927
1273
|
|
|
928
1274
|
// Build query with cursor-based pagination if available
|
|
929
1275
|
if (!tab.limit) {
|
|
@@ -940,7 +1286,8 @@ async function loadTableData() {
|
|
|
940
1286
|
}
|
|
941
1287
|
|
|
942
1288
|
const response = await fetch(`/api/tables/${tab.tableName}?${queryString}`, {
|
|
943
|
-
headers: { 'x-connection-id': tab.connectionId }
|
|
1289
|
+
headers: { 'x-connection-id': tab.connectionId },
|
|
1290
|
+
signal: signal
|
|
944
1291
|
});
|
|
945
1292
|
const data = await response.json();
|
|
946
1293
|
|
|
@@ -964,115 +1311,227 @@ async function loadTableData() {
|
|
|
964
1311
|
tab.cursor = data.nextCursor;
|
|
965
1312
|
}
|
|
966
1313
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1314
|
+
tab.abortController = null; // Request finished successfully
|
|
1315
|
+
|
|
1316
|
+
if (!data.rows) {
|
|
1317
|
+
|
|
970
1318
|
tab.data = null;
|
|
1319
|
+
tableView.innerHTML = '<div class="error-state"><p>Invalid data received</p></div>';
|
|
971
1320
|
return;
|
|
972
1321
|
}
|
|
973
1322
|
|
|
974
1323
|
renderTable(data);
|
|
975
1324
|
renderPagination();
|
|
976
1325
|
} catch (error) {
|
|
977
|
-
|
|
978
|
-
|
|
1326
|
+
if (error.name === 'AbortError') {
|
|
1327
|
+
console.log('Request cancelled');
|
|
1328
|
+
} else {
|
|
1329
|
+
tableView.innerHTML = '<div class="error-state"><p>Error: ' + error.message + '</p></div>';
|
|
1330
|
+
pagination.style.display = 'none';
|
|
1331
|
+
}
|
|
979
1332
|
} finally {
|
|
980
|
-
hideLoading();
|
|
981
1333
|
}
|
|
982
1334
|
}
|
|
983
1335
|
|
|
984
|
-
function
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
const columns = Object.keys(data.rows[0] || {});
|
|
1336
|
+
function handleCancelLoading(tab) {
|
|
1337
|
+
if (tab && tab.abortController) {
|
|
1338
|
+
tab.abortController.abort();
|
|
1339
|
+
tab.abortController = null;
|
|
989
1340
|
|
|
990
|
-
|
|
991
|
-
|
|
1341
|
+
if (tab.data) {
|
|
1342
|
+
renderTable(tab.data);
|
|
1343
|
+
renderPagination();
|
|
1344
|
+
} else {
|
|
1345
|
+
// No previous data (fresh load), close the tab
|
|
1346
|
+
const index = tabs.indexOf(tab);
|
|
1347
|
+
if (index !== -1) {
|
|
1348
|
+
closeTab(index);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
992
1351
|
}
|
|
1352
|
+
}
|
|
993
1353
|
|
|
1354
|
+
function renderTableHeader(tab, columns = [], isShimmer = false) {
|
|
994
1355
|
const tableHeader = document.createElement('div');
|
|
995
1356
|
tableHeader.className = 'table-header';
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1357
|
+
|
|
1358
|
+
let rangeInfo = '';
|
|
1359
|
+
if (isShimmer) {
|
|
1360
|
+
rangeInfo = '<span class="skeleton" style="width: 150px; display: inline-block;"></span>';
|
|
1361
|
+
} else {
|
|
1362
|
+
const startRow = ((tab.page - 1) * tab.limit) + 1;
|
|
1363
|
+
const endRow = Math.min(tab.page * tab.limit, tab.totalCount);
|
|
1364
|
+
const totalRows = tab.totalCount;
|
|
1365
|
+
|
|
1366
|
+
rangeInfo = `
|
|
1367
|
+
<span class="row-info-range">${startRow.toLocaleString()}–${endRow.toLocaleString()}</span>
|
|
1368
|
+
<span class="row-info-separator">of</span>
|
|
1369
|
+
<span class="row-info-total">${totalRows.toLocaleString()}</span>
|
|
1370
|
+
${tab.isApproximate ? '<span class="row-info-approx">(approx.)</span>' : ''}
|
|
1371
|
+
`;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
let actions = '';
|
|
1375
|
+
if (isShimmer) {
|
|
1376
|
+
actions = `
|
|
1377
|
+
<button class="cancel-button" id="cancelButton">
|
|
1378
|
+
<span>Cancel</span>
|
|
1379
|
+
</button>
|
|
1380
|
+
`;
|
|
1381
|
+
} else {
|
|
1382
|
+
actions = `
|
|
1383
|
+
<button class="refresh-button" id="refreshButton" title="Refresh data">
|
|
1384
|
+
<svg class="refresh-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1385
|
+
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path>
|
|
1386
|
+
<path d="M21 3v5h-5"></path>
|
|
1387
|
+
</svg>
|
|
1388
|
+
<span class="refresh-text">Refresh</span>
|
|
1389
|
+
</button>
|
|
1390
|
+
<div class="limit-selector">
|
|
1391
|
+
<select id="limitSelect" class="limit-select" title="Rows per page">
|
|
1392
|
+
<option value="25" ${tab.limit === 25 ? 'selected' : ''}>25 rows</option>
|
|
1393
|
+
<option value="50" ${tab.limit === 50 ? 'selected' : ''}>50 rows</option>
|
|
1394
|
+
<option value="100" ${tab.limit === 100 ? 'selected' : ''}>100 rows</option>
|
|
1395
|
+
<option value="200" ${tab.limit === 200 ? 'selected' : ''}>200 rows</option>
|
|
1396
|
+
<option value="500" ${tab.limit === 500 ? 'selected' : ''}>500 rows</option>
|
|
1397
|
+
</select>
|
|
1398
|
+
</div>
|
|
1399
|
+
<div class="column-selector">
|
|
1400
|
+
<button class="column-button" id="columnButton" title="Show/Hide columns">
|
|
1401
|
+
<svg class="column-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1402
|
+
<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"/>
|
|
1403
|
+
<path d="M6 2V14M10 2V14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
1404
|
+
</svg>
|
|
1405
|
+
<span class="column-label" id="columnLabel">Columns</span>
|
|
1406
|
+
</button>
|
|
1407
|
+
<div class="column-menu" id="columnMenu" style="display: none;">
|
|
1408
|
+
<div class="column-menu-header">Columns</div>
|
|
1409
|
+
<div class="column-menu-options" id="columnMenuOptions"></div>
|
|
1410
|
+
</div>
|
|
1411
|
+
</div>
|
|
1412
|
+
`;
|
|
1413
|
+
}
|
|
999
1414
|
|
|
1000
1415
|
tableHeader.innerHTML = `
|
|
1001
1416
|
<div class="table-header-left">
|
|
1002
1417
|
<h2>${tab.tableName}</h2>
|
|
1003
1418
|
<div class="table-header-actions">
|
|
1004
|
-
|
|
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>
|
|
1010
|
-
</button>
|
|
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>
|
|
1032
|
-
</div>
|
|
1419
|
+
${actions}
|
|
1033
1420
|
</div>
|
|
1034
1421
|
</div>
|
|
1035
1422
|
<div class="row-info-container">
|
|
1036
1423
|
<span class="row-info">
|
|
1037
|
-
|
|
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>' : ''}
|
|
1424
|
+
${rangeInfo}
|
|
1041
1425
|
</span>
|
|
1042
1426
|
</div>
|
|
1043
1427
|
`;
|
|
1044
1428
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1429
|
+
if (isShimmer) {
|
|
1430
|
+
const cancelButton = tableHeader.querySelector('#cancelButton');
|
|
1431
|
+
if (cancelButton) {
|
|
1432
|
+
cancelButton.addEventListener('click', () => handleCancelLoading(tab));
|
|
1433
|
+
}
|
|
1434
|
+
} else {
|
|
1435
|
+
const refreshButton = tableHeader.querySelector('#refreshButton');
|
|
1436
|
+
if (refreshButton) {
|
|
1437
|
+
refreshButton.addEventListener('click', handleRefresh);
|
|
1438
|
+
}
|
|
1049
1439
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1440
|
+
const limitSelect = tableHeader.querySelector('#limitSelect');
|
|
1441
|
+
if (limitSelect) {
|
|
1442
|
+
limitSelect.addEventListener('change', (e) => {
|
|
1443
|
+
const newLimit = parseInt(e.target.value, 10);
|
|
1444
|
+
if (tab.limit !== newLimit) {
|
|
1445
|
+
tab.limit = newLimit;
|
|
1446
|
+
tab.page = 1;
|
|
1447
|
+
tab.cursor = null;
|
|
1448
|
+
tab.cursorHistory = [];
|
|
1449
|
+
tab.data = null;
|
|
1450
|
+
loadTableData();
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
if (!tab.hiddenColumns) tab.hiddenColumns = [];
|
|
1456
|
+
if (!tab.columnWidths) tab.columnWidths = {};
|
|
1457
|
+
|
|
1458
|
+
setupColumnSelector(tab, columns, tableHeader);
|
|
1459
|
+
updateColumnButtonLabel(tab, columns, tableHeader);
|
|
1063
1460
|
}
|
|
1064
1461
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1462
|
+
return tableHeader;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function renderShimmerTable(tab) {
|
|
1466
|
+
let columns = [];
|
|
1467
|
+
if (tab.data && tab.data.rows && tab.data.rows.length > 0) {
|
|
1468
|
+
columns = Object.keys(tab.data.rows[0]);
|
|
1469
|
+
} else {
|
|
1470
|
+
columns = ['Column 1', 'Column 2', 'Column 3', 'Column 4', 'Column 5'];
|
|
1067
1471
|
}
|
|
1068
1472
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1473
|
+
const tableHeader = renderTableHeader(tab, columns, true);
|
|
1474
|
+
const tableContainer = document.createElement('div');
|
|
1475
|
+
tableContainer.className = 'table-container';
|
|
1476
|
+
|
|
1477
|
+
const table = document.createElement('table');
|
|
1478
|
+
const thead = document.createElement('thead');
|
|
1479
|
+
const headerRow = document.createElement('tr');
|
|
1480
|
+
|
|
1481
|
+
// Render header placeholders
|
|
1482
|
+
columns.forEach(col => {
|
|
1483
|
+
const th = document.createElement('th');
|
|
1484
|
+
th.className = 'resizable';
|
|
1485
|
+
const skeleton = document.createElement('div');
|
|
1486
|
+
skeleton.className = 'skeleton';
|
|
1487
|
+
skeleton.style.width = '80px';
|
|
1488
|
+
th.appendChild(skeleton);
|
|
1489
|
+
headerRow.appendChild(th);
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
thead.appendChild(headerRow);
|
|
1493
|
+
table.appendChild(thead);
|
|
1494
|
+
|
|
1495
|
+
const tbody = document.createElement('tbody');
|
|
1496
|
+
// Render generic shimmer rows
|
|
1497
|
+
for (let i = 0; i < 10; i++) {
|
|
1498
|
+
const tr = document.createElement('tr');
|
|
1499
|
+
tr.className = 'shimmer-row';
|
|
1500
|
+
columns.forEach(() => {
|
|
1501
|
+
const td = document.createElement('td');
|
|
1502
|
+
const skeleton = document.createElement('div');
|
|
1503
|
+
skeleton.className = 'skeleton shimmer-cell';
|
|
1504
|
+
td.appendChild(skeleton);
|
|
1505
|
+
tr.appendChild(td);
|
|
1506
|
+
});
|
|
1507
|
+
tbody.appendChild(tr);
|
|
1071
1508
|
}
|
|
1072
1509
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1510
|
+
table.appendChild(tbody);
|
|
1511
|
+
tableContainer.appendChild(table);
|
|
1512
|
+
|
|
1513
|
+
tableView.innerHTML = '';
|
|
1514
|
+
tableView.appendChild(tableHeader);
|
|
1515
|
+
tableView.appendChild(tableContainer);
|
|
1516
|
+
|
|
1517
|
+
// Hide pagination during loading
|
|
1518
|
+
pagination.style.display = 'none';
|
|
1519
|
+
}
|
|
1075
1520
|
|
|
1521
|
+
|
|
1522
|
+
function renderTable(data) {
|
|
1523
|
+
const tab = getActiveTab();
|
|
1524
|
+
if (!tab) return;
|
|
1525
|
+
|
|
1526
|
+
const columns = (data.rows && data.rows.length > 0)
|
|
1527
|
+
? Object.keys(data.rows[0])
|
|
1528
|
+
: (data.columns ? Object.keys(data.columns) : []);
|
|
1529
|
+
|
|
1530
|
+
if (!tab.limit) {
|
|
1531
|
+
tab.limit = 100; // Default limit for existing tabs
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
const tableHeader = renderTableHeader(tab, columns);
|
|
1076
1535
|
const tableContainer = document.createElement('div');
|
|
1077
1536
|
tableContainer.className = 'table-container';
|
|
1078
1537
|
|
|
@@ -1199,6 +1658,7 @@ function renderTable(data) {
|
|
|
1199
1658
|
thead.appendChild(headerRow);
|
|
1200
1659
|
table.appendChild(thead);
|
|
1201
1660
|
|
|
1661
|
+
|
|
1202
1662
|
const tbody = document.createElement('tbody');
|
|
1203
1663
|
// Server-side sorting, so rows are already sorted
|
|
1204
1664
|
const rows = data.rows || [];
|
|
@@ -1977,6 +2437,18 @@ function closeCellContentPopup() {
|
|
|
1977
2437
|
}
|
|
1978
2438
|
}
|
|
1979
2439
|
|
|
2440
|
+
function isAppBusy() {
|
|
2441
|
+
return tabs.some(tab => tab.abortController !== null);
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
function cancelAllActiveRequests() {
|
|
2445
|
+
tabs.forEach(tab => {
|
|
2446
|
+
if (tab.abortController) {
|
|
2447
|
+
handleCancelLoading(tab);
|
|
2448
|
+
}
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
|
|
1980
2452
|
function showLoading() {
|
|
1981
2453
|
if (loadingOverlay) {
|
|
1982
2454
|
loadingOverlay.style.display = 'flex';
|