pglens 2.1.0 → 2.2.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/.github/workflows/release.yml +30 -0
- package/CHANGELOG.md +17 -1
- package/README.md +37 -4
- package/client/app.js +321 -9
- package/client/index.html +37 -1
- package/client/styles.css +351 -2
- package/electron/assets/icon.ico +0 -0
- package/electron/assets/icon.png +0 -0
- package/electron/main.js +85 -0
- package/package.json +63 -3
- package/src/db/connection.js +104 -10
- package/src/server.js +29 -42
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
tags:
|
|
7
|
+
- "v*"
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
os: [macos-latest, ubuntu-latest, windows-latest]
|
|
14
|
+
runs-on: ${{ matrix.os }}
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: 20
|
|
21
|
+
- run: npm ci
|
|
22
|
+
- run: npm run dist
|
|
23
|
+
- uses: softprops/action-gh-release@v1
|
|
24
|
+
with:
|
|
25
|
+
files: |
|
|
26
|
+
dist/*.dmg
|
|
27
|
+
dist/*.zip
|
|
28
|
+
dist/*.exe
|
|
29
|
+
dist/*.AppImage
|
|
30
|
+
dist/*.deb
|
package/CHANGELOG.md
CHANGED
|
@@ -5,18 +5,35 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.2.0] - 2026-02-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Row Numbers**: Table rows now display row numbers for easier navigation and reference.
|
|
13
|
+
- **Table Schema Viewer**: View table structure and column definitions directly from the UI.
|
|
14
|
+
- **Spotlight Search**: Quick table search with `Cmd+K` / `Ctrl+K` keyboard shortcut for fast navigation.
|
|
15
|
+
- **Connection Persistence**: Desktop app now saves connections and restores them on restart.
|
|
16
|
+
- **Auto-Updates**: Desktop app automatically checks for updates and notifies when new versions are available.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Cleaner codebase with reduced unnecessary comments.
|
|
21
|
+
|
|
8
22
|
## [2.1.0] - 2026-01-11
|
|
9
23
|
|
|
10
24
|
### Added
|
|
25
|
+
|
|
11
26
|
- **Default Landing Page**: The app now defaults to an "All Connections" grid view on startup.
|
|
12
27
|
- **Simplified Sidebar**: Connection management (Edit/Delete) is now centralized on the Landing Page.
|
|
13
28
|
- **Smart View Switching**: Table list and search are hidden until a server is explicitly selected.
|
|
14
29
|
|
|
15
30
|
### Changed
|
|
31
|
+
|
|
16
32
|
- **Navigation Flow**: "Add Connection" and Logo clicks efficiently return you to the Landing Page.
|
|
17
33
|
- **Auto-Connect Disabled**: Adding a new connection returns to the grid instead of auto-opening the server.
|
|
18
34
|
|
|
19
35
|
### Fixed
|
|
36
|
+
|
|
20
37
|
- **View Persistence**: Reconnecting to an active server no longer overwrites open tabs with a loading screen.
|
|
21
38
|
- **Connection Updates**: Fixed issue where the connection grid would not update immediately after adding a server.
|
|
22
39
|
|
|
@@ -83,4 +100,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
83
100
|
|
|
84
101
|
[1.1.0]: https://github.com/tsvillain/pglens/compare/v1.0.0...v1.1.0
|
|
85
102
|
[1.0.0]: https://github.com/tsvillain/pglens/releases/tag/v1.0.0
|
|
86
|
-
|
package/README.md
CHANGED
|
@@ -5,9 +5,13 @@ A simple PostgreSQL database viewer tool. Perfect to quickly view and explore yo
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- 🔌 **Connection Manager**: Manage multiple database connections from a single UI
|
|
8
|
+
- 💾 **Connection Persistence**: Saved connections are restored when you reopen the app
|
|
8
9
|
- 🚀 **Background Service**: Runs as a daemon process for persistent access
|
|
9
10
|
- 🗂️ **Table Browser**: View all tables in your database in a clean, searchable sidebar
|
|
11
|
+
- 🔎 **Spotlight Search**: Quick table search with `Cmd+K` / `Ctrl+K` for fast navigation
|
|
10
12
|
- 📊 **Data Viewer**: Browse table rows with a modern, easy-to-read interface
|
|
13
|
+
- 🔢 **Row Numbers**: Row numbers displayed for easier navigation and reference
|
|
14
|
+
- 📋 **Table Schema**: View table structure and column definitions directly from the UI
|
|
11
15
|
- 📝 **Cell Content Viewer**: Double-click any cell to view full content in a popup
|
|
12
16
|
- 🎨 **JSON/JSONB Formatting**: Auto-formats JSON data with syntax highlighting
|
|
13
17
|
- 🕒 **Timezone Support**: View timestamps in local, UTC, or other timezones
|
|
@@ -21,6 +25,7 @@ A simple PostgreSQL database viewer tool. Perfect to quickly view and explore yo
|
|
|
21
25
|
- 🎨 **Theme Support**: Choose between light, dark, or system theme
|
|
22
26
|
- ⚡ **Optimized Performance**: Uses cursor-based pagination for efficient large table navigation
|
|
23
27
|
- 🔒 **SSL Support**: Configurable SSL modes (Disable, Require, Prefer, Verify CA/Full)
|
|
28
|
+
- 🔄 **Auto-Updates**: Desktop app automatically checks for and installs updates
|
|
24
29
|
- 🚀 **Easy Setup**: Install globally and run with a single command
|
|
25
30
|
|
|
26
31
|
## Installation
|
|
@@ -54,8 +59,6 @@ pglens url
|
|
|
54
59
|
|
|
55
60
|
### Connect to a Database
|
|
56
61
|
|
|
57
|
-
### Connect to a Database
|
|
58
|
-
|
|
59
62
|
1. Open `http://localhost:54321` to see the **All Connections** landing page.
|
|
60
63
|
2. Click the **Add Connection** card or the **+** icon in the grid.
|
|
61
64
|
3. Enter your connection details using one of the tabs:
|
|
@@ -77,7 +80,7 @@ pglens stop
|
|
|
77
80
|
|
|
78
81
|
1. **Start**: Run `pglens start` to launch the background service
|
|
79
82
|
2. **Connect**: Add one or more database connections via the Web UI
|
|
80
|
-
3. **Explore**:
|
|
83
|
+
3. **Explore**:
|
|
81
84
|
- Use the sidebar to browse tables across different connections
|
|
82
85
|
- Double-click cells to view detailed content
|
|
83
86
|
- Use the "Columns" menu to toggle visibility
|
|
@@ -92,10 +95,40 @@ To develop or modify pglens:
|
|
|
92
95
|
git clone https://github.com/tsvillain/pglens.git
|
|
93
96
|
cd pglens
|
|
94
97
|
|
|
98
|
+
# Install dependencies
|
|
95
99
|
# Install dependencies
|
|
96
100
|
npm install
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Run Desktop App
|
|
104
|
+
|
|
105
|
+
To run the application as a standalone desktop app during development:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm run electron:start
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Build Desktop App
|
|
112
|
+
|
|
113
|
+
To build the desktop application for your current platform:
|
|
97
114
|
|
|
98
|
-
|
|
115
|
+
```bash
|
|
116
|
+
npm run dist
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
To build for specific platforms (requires supported environment):
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npm run dist:mac # Build for macOS
|
|
123
|
+
npm run dist:win # Build for Windows
|
|
124
|
+
npm run dist:linux # Build for Linux
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Run Server Locally
|
|
128
|
+
|
|
129
|
+
To run the server locally in foreground:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
99
132
|
node bin/pglens serve
|
|
100
133
|
```
|
|
101
134
|
|
package/client/app.js
CHANGED
|
@@ -60,6 +60,19 @@ const connectButton = document.getElementById('connectButton');
|
|
|
60
60
|
const connectionError = document.getElementById('connectionError');
|
|
61
61
|
const loadingOverlay = document.getElementById('loadingOverlay');
|
|
62
62
|
|
|
63
|
+
// Spotlight Elements
|
|
64
|
+
const spotlightOverlay = document.getElementById('spotlightOverlay');
|
|
65
|
+
const spotlightInput = document.getElementById('spotlightInput');
|
|
66
|
+
const spotlightResults = document.getElementById('spotlightResults');
|
|
67
|
+
let spotlightSelectedIndex = -1;
|
|
68
|
+
let spotlightMatches = [];
|
|
69
|
+
|
|
70
|
+
// Schema Dialog UI Elements
|
|
71
|
+
const schemaDialog = document.getElementById('schemaDialog');
|
|
72
|
+
const closeSchemaDialogBtn = document.getElementById('closeSchemaDialog');
|
|
73
|
+
const closeSchemaButton = document.getElementById('closeSchemaButton');
|
|
74
|
+
const schemaTableContainer = document.getElementById('schemaTableContainer');
|
|
75
|
+
|
|
63
76
|
/**
|
|
64
77
|
* Initialize the application when DOM is ready.
|
|
65
78
|
* Sets up event listeners and loads initial data.
|
|
@@ -112,9 +125,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
112
125
|
});
|
|
113
126
|
|
|
114
127
|
sidebarToggle.addEventListener('click', () => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
128
|
+
sidebar.classList.toggle('minimized');
|
|
129
|
+
updateSidebarToggleState();
|
|
118
130
|
});
|
|
119
131
|
|
|
120
132
|
updateSidebarToggleState();
|
|
@@ -163,6 +175,66 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
163
175
|
if (newConnectionBtn) {
|
|
164
176
|
newConnectionBtn.addEventListener('click', () => showConnectionDialog(true));
|
|
165
177
|
}
|
|
178
|
+
|
|
179
|
+
// Schema Dialog Listeners
|
|
180
|
+
if (closeSchemaDialogBtn) {
|
|
181
|
+
closeSchemaDialogBtn.addEventListener('click', hideSchemaDialog);
|
|
182
|
+
}
|
|
183
|
+
if (closeSchemaButton) {
|
|
184
|
+
closeSchemaButton.addEventListener('click', hideSchemaDialog);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Close schema dialog on outside click
|
|
188
|
+
if (schemaDialog) {
|
|
189
|
+
schemaDialog.addEventListener('click', (e) => {
|
|
190
|
+
if (e.target === schemaDialog) {
|
|
191
|
+
hideSchemaDialog();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Spotlight Search Shortcut (Cmd+K / Ctrl+K)
|
|
197
|
+
document.addEventListener('keydown', (e) => {
|
|
198
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
|
|
199
|
+
e.preventDefault();
|
|
200
|
+
// Only toggle if tables are loaded
|
|
201
|
+
if (allTables && allTables.length > 0) {
|
|
202
|
+
toggleSpotlight();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Spotlight Navigation
|
|
207
|
+
if (spotlightOverlay.style.display !== 'none') {
|
|
208
|
+
if (e.key === 'Escape') {
|
|
209
|
+
toggleSpotlight(false);
|
|
210
|
+
} else if (e.key === 'ArrowDown') {
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
navigateSpotlight(1);
|
|
213
|
+
} else if (e.key === 'ArrowUp') {
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
navigateSpotlight(-1);
|
|
216
|
+
} else if (e.key === 'Enter') {
|
|
217
|
+
e.preventDefault();
|
|
218
|
+
executeSpotlightSelection();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Spotlight Input Listener
|
|
224
|
+
if (spotlightInput) {
|
|
225
|
+
spotlightInput.addEventListener('input', (e) => {
|
|
226
|
+
filterSpotlight(e.target.value);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Close spotlight on overlay click
|
|
231
|
+
if (spotlightOverlay) {
|
|
232
|
+
spotlightOverlay.addEventListener('click', (e) => {
|
|
233
|
+
if (e.target === spotlightOverlay) {
|
|
234
|
+
toggleSpotlight(false);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
166
238
|
});
|
|
167
239
|
|
|
168
240
|
/**
|
|
@@ -216,11 +288,7 @@ function updateThemeIcon() {
|
|
|
216
288
|
}
|
|
217
289
|
|
|
218
290
|
function updateSidebarToggleState() {
|
|
219
|
-
if (
|
|
220
|
-
sidebarToggle.disabled = true;
|
|
221
|
-
sidebarToggle.classList.add('disabled');
|
|
222
|
-
sidebar.classList.remove('minimized');
|
|
223
|
-
} else {
|
|
291
|
+
if (sidebarToggle) {
|
|
224
292
|
sidebarToggle.disabled = false;
|
|
225
293
|
sidebarToggle.classList.remove('disabled');
|
|
226
294
|
}
|
|
@@ -286,6 +354,26 @@ function renderConnectionsList() {
|
|
|
286
354
|
li.classList.add('active');
|
|
287
355
|
}
|
|
288
356
|
|
|
357
|
+
// Generate Initials
|
|
358
|
+
let initials = '';
|
|
359
|
+
if (conn.name) {
|
|
360
|
+
const parts = conn.name.trim().split(/\s+/);
|
|
361
|
+
if (parts.length >= 2) {
|
|
362
|
+
initials = (parts[0][0] + parts[1][0]).toUpperCase();
|
|
363
|
+
} else if (conn.name.length >= 2) {
|
|
364
|
+
initials = conn.name.substring(0, 2).toUpperCase();
|
|
365
|
+
} else {
|
|
366
|
+
initials = conn.name.substring(0, 1).toUpperCase();
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
initials = 'DB';
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const initialsDiv = document.createElement('div');
|
|
373
|
+
initialsDiv.className = 'connection-initials';
|
|
374
|
+
initialsDiv.textContent = initials;
|
|
375
|
+
li.appendChild(initialsDiv);
|
|
376
|
+
|
|
289
377
|
const nameSpan = document.createElement('span');
|
|
290
378
|
nameSpan.className = 'connection-name';
|
|
291
379
|
nameSpan.textContent = conn.name;
|
|
@@ -301,6 +389,9 @@ function renderConnectionsList() {
|
|
|
301
389
|
});
|
|
302
390
|
|
|
303
391
|
connectionsList.appendChild(li);
|
|
392
|
+
|
|
393
|
+
// Add tooltip for minimized state
|
|
394
|
+
li.title = conn.name;
|
|
304
395
|
});
|
|
305
396
|
}
|
|
306
397
|
|
|
@@ -1299,6 +1390,7 @@ async function loadTableData() {
|
|
|
1299
1390
|
tab.hasPrimaryKey = data.hasPrimaryKey || false;
|
|
1300
1391
|
tab.isApproximate = data.isApproximate || false;
|
|
1301
1392
|
tab.data = data; // Cache data for client-side sorting
|
|
1393
|
+
tab.columns = data.columns; // Store column metadata for schema view
|
|
1302
1394
|
|
|
1303
1395
|
// Update cursor for next page navigation
|
|
1304
1396
|
if (data.nextCursor) {
|
|
@@ -1387,6 +1479,14 @@ function renderTableHeader(tab, columns = [], isShimmer = false) {
|
|
|
1387
1479
|
</svg>
|
|
1388
1480
|
<span class="refresh-text">Refresh</span>
|
|
1389
1481
|
</button>
|
|
1482
|
+
<button class="menu-button" id="schemaButton" title="View Schema">
|
|
1483
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1484
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
1485
|
+
<line x1="3" y1="9" x2="21" y2="9"></line>
|
|
1486
|
+
<line x1="9" y1="21" x2="9" y2="9"></line>
|
|
1487
|
+
</svg>
|
|
1488
|
+
<span>Schema</span>
|
|
1489
|
+
</button>
|
|
1390
1490
|
<div class="limit-selector">
|
|
1391
1491
|
<select id="limitSelect" class="limit-select" title="Rows per page">
|
|
1392
1492
|
<option value="25" ${tab.limit === 25 ? 'selected' : ''}>25 rows</option>
|
|
@@ -1437,6 +1537,13 @@ function renderTableHeader(tab, columns = [], isShimmer = false) {
|
|
|
1437
1537
|
refreshButton.addEventListener('click', handleRefresh);
|
|
1438
1538
|
}
|
|
1439
1539
|
|
|
1540
|
+
const schemaButton = tableHeader.querySelector('#schemaButton');
|
|
1541
|
+
if (schemaButton) {
|
|
1542
|
+
schemaButton.addEventListener('click', () => {
|
|
1543
|
+
showSchemaModal(tab.tableName, tab.columns);
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1440
1547
|
const limitSelect = tableHeader.querySelector('#limitSelect');
|
|
1441
1548
|
if (limitSelect) {
|
|
1442
1549
|
limitSelect.addEventListener('change', (e) => {
|
|
@@ -1479,6 +1586,14 @@ function renderShimmerTable(tab) {
|
|
|
1479
1586
|
const headerRow = document.createElement('tr');
|
|
1480
1587
|
|
|
1481
1588
|
// Render header placeholders
|
|
1589
|
+
const rowNumTh = document.createElement('th');
|
|
1590
|
+
rowNumTh.className = 'row-number-header';
|
|
1591
|
+
const rowNumSkeleton = document.createElement('div');
|
|
1592
|
+
rowNumSkeleton.className = 'skeleton';
|
|
1593
|
+
rowNumSkeleton.style.width = '30px';
|
|
1594
|
+
rowNumTh.appendChild(rowNumSkeleton);
|
|
1595
|
+
headerRow.appendChild(rowNumTh);
|
|
1596
|
+
|
|
1482
1597
|
columns.forEach(col => {
|
|
1483
1598
|
const th = document.createElement('th');
|
|
1484
1599
|
th.className = 'resizable';
|
|
@@ -1497,6 +1612,15 @@ function renderShimmerTable(tab) {
|
|
|
1497
1612
|
for (let i = 0; i < 10; i++) {
|
|
1498
1613
|
const tr = document.createElement('tr');
|
|
1499
1614
|
tr.className = 'shimmer-row';
|
|
1615
|
+
|
|
1616
|
+
// Row number shimmer cell
|
|
1617
|
+
const rowNumTd = document.createElement('td');
|
|
1618
|
+
const rowNumSkeleton = document.createElement('div');
|
|
1619
|
+
rowNumSkeleton.className = 'skeleton shimmer-cell';
|
|
1620
|
+
rowNumSkeleton.style.width = '20px';
|
|
1621
|
+
rowNumTd.appendChild(rowNumSkeleton);
|
|
1622
|
+
tr.appendChild(rowNumTd);
|
|
1623
|
+
|
|
1500
1624
|
columns.forEach(() => {
|
|
1501
1625
|
const td = document.createElement('td');
|
|
1502
1626
|
const skeleton = document.createElement('div');
|
|
@@ -1542,6 +1666,12 @@ function renderTable(data) {
|
|
|
1542
1666
|
const thead = document.createElement('thead');
|
|
1543
1667
|
const headerRow = document.createElement('tr');
|
|
1544
1668
|
|
|
1669
|
+
// Add Row Number Header
|
|
1670
|
+
const rowNumTh = document.createElement('th');
|
|
1671
|
+
rowNumTh.className = 'row-number-header';
|
|
1672
|
+
rowNumTh.textContent = '#';
|
|
1673
|
+
headerRow.appendChild(rowNumTh);
|
|
1674
|
+
|
|
1545
1675
|
visibleColumns.forEach((column, index) => {
|
|
1546
1676
|
const th = document.createElement('th');
|
|
1547
1677
|
th.className = 'sortable resizable';
|
|
@@ -1663,8 +1793,16 @@ function renderTable(data) {
|
|
|
1663
1793
|
// Server-side sorting, so rows are already sorted
|
|
1664
1794
|
const rows = data.rows || [];
|
|
1665
1795
|
|
|
1666
|
-
rows.forEach(row => {
|
|
1796
|
+
rows.forEach((row, rowIndex) => {
|
|
1667
1797
|
const tr = document.createElement('tr');
|
|
1798
|
+
|
|
1799
|
+
// Add Row Number Cell
|
|
1800
|
+
const rowNumTd = document.createElement('td');
|
|
1801
|
+
rowNumTd.className = 'row-number-cell';
|
|
1802
|
+
const rowNumber = ((tab.page - 1) * tab.limit) + rowIndex + 1;
|
|
1803
|
+
rowNumTd.textContent = rowNumber.toLocaleString();
|
|
1804
|
+
tr.appendChild(rowNumTd);
|
|
1805
|
+
|
|
1668
1806
|
visibleColumns.forEach(column => {
|
|
1669
1807
|
const td = document.createElement('td');
|
|
1670
1808
|
|
|
@@ -2460,3 +2598,177 @@ function hideLoading() {
|
|
|
2460
2598
|
loadingOverlay.style.display = 'none';
|
|
2461
2599
|
}
|
|
2462
2600
|
}
|
|
2601
|
+
|
|
2602
|
+
/**
|
|
2603
|
+
* Show the schema modal for a table.
|
|
2604
|
+
* @param {string} tableName - Name of the table
|
|
2605
|
+
* @param {Object} columns - Column metadata
|
|
2606
|
+
*/
|
|
2607
|
+
function showSchemaModal(tableName, columns) {
|
|
2608
|
+
const title = schemaDialog.querySelector('h2');
|
|
2609
|
+
title.textContent = `Schema: ${tableName}`;
|
|
2610
|
+
|
|
2611
|
+
renderSchemaTable(columns);
|
|
2612
|
+
schemaDialog.style.display = 'flex';
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
function hideSchemaDialog() {
|
|
2616
|
+
schemaDialog.style.display = 'none';
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
/**
|
|
2620
|
+
* Render the schema table inside the modal.
|
|
2621
|
+
* @param {Object} columns - Column metadata
|
|
2622
|
+
*/
|
|
2623
|
+
function renderSchemaTable(columns) {
|
|
2624
|
+
schemaTableContainer.innerHTML = '';
|
|
2625
|
+
|
|
2626
|
+
const table = document.createElement('table');
|
|
2627
|
+
table.className = 'schema-table';
|
|
2628
|
+
|
|
2629
|
+
const thead = document.createElement('thead');
|
|
2630
|
+
thead.innerHTML = `
|
|
2631
|
+
<tr>
|
|
2632
|
+
<th>Column</th>
|
|
2633
|
+
<th>Type</th>
|
|
2634
|
+
<th>Key</th>
|
|
2635
|
+
</tr>
|
|
2636
|
+
`;
|
|
2637
|
+
table.appendChild(thead);
|
|
2638
|
+
|
|
2639
|
+
const tbody = document.createElement('tbody');
|
|
2640
|
+
|
|
2641
|
+
Object.entries(columns).forEach(([name, meta]) => {
|
|
2642
|
+
const tr = document.createElement('tr');
|
|
2643
|
+
|
|
2644
|
+
// Key Badges
|
|
2645
|
+
let keyBadges = '';
|
|
2646
|
+
if (meta.isPrimaryKey) {
|
|
2647
|
+
keyBadges += '<span class="schema-badge pk">PK</span> ';
|
|
2648
|
+
}
|
|
2649
|
+
if (meta.isForeignKey) {
|
|
2650
|
+
keyBadges += '<span class="schema-badge fk">FK</span> ';
|
|
2651
|
+
}
|
|
2652
|
+
if (meta.isUnique && !meta.isPrimaryKey) {
|
|
2653
|
+
keyBadges += '<span class="schema-badge unique">UQ</span> ';
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
// FK Reference
|
|
2657
|
+
let fkRef = '';
|
|
2658
|
+
if (meta.isForeignKey && meta.foreignKeyRef) {
|
|
2659
|
+
fkRef = `<div class="fk-ref">→ ${meta.foreignKeyRef.table}(${meta.foreignKeyRef.column})</div>`;
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
tr.innerHTML = `
|
|
2663
|
+
<td style="font-weight: 500">${name}</td>
|
|
2664
|
+
<td style="color: var(--text-secondary); font-family: monospace">${meta.dataType}</td>
|
|
2665
|
+
<td>
|
|
2666
|
+
${keyBadges}
|
|
2667
|
+
${fkRef}
|
|
2668
|
+
</td>
|
|
2669
|
+
`;
|
|
2670
|
+
|
|
2671
|
+
tbody.appendChild(tr);
|
|
2672
|
+
});
|
|
2673
|
+
|
|
2674
|
+
table.appendChild(tbody);
|
|
2675
|
+
schemaTableContainer.appendChild(table);
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
// Spotlight Functions
|
|
2679
|
+
function toggleSpotlight(show) {
|
|
2680
|
+
if (show === undefined) {
|
|
2681
|
+
show = spotlightOverlay.style.display === 'none';
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
if (show) {
|
|
2685
|
+
spotlightOverlay.style.display = 'flex';
|
|
2686
|
+
spotlightInput.value = '';
|
|
2687
|
+
spotlightInput.focus();
|
|
2688
|
+
filterSpotlight('');
|
|
2689
|
+
} else {
|
|
2690
|
+
spotlightOverlay.style.display = 'none';
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
function filterSpotlight(query) {
|
|
2695
|
+
spotlightResults.innerHTML = '';
|
|
2696
|
+
spotlightSelectedIndex = 0;
|
|
2697
|
+
|
|
2698
|
+
if (!allTables || allTables.length === 0) {
|
|
2699
|
+
spotlightResults.innerHTML = '<div class="spotlight-empty">No tables available</div>';
|
|
2700
|
+
spotlightMatches = [];
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
const lowerQuery = query.toLowerCase().trim();
|
|
2705
|
+
spotlightMatches = allTables.filter(t => t.toLowerCase().includes(lowerQuery));
|
|
2706
|
+
|
|
2707
|
+
if (spotlightMatches.length === 0) {
|
|
2708
|
+
spotlightResults.innerHTML = '<div class="spotlight-empty">No tables found</div>';
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
spotlightMatches.forEach((table, index) => {
|
|
2713
|
+
const div = document.createElement('div');
|
|
2714
|
+
div.className = `spotlight-result-item ${index === 0 ? 'selected' : ''}`;
|
|
2715
|
+
div.dataset.index = index;
|
|
2716
|
+
|
|
2717
|
+
div.innerHTML = `
|
|
2718
|
+
<div class="spotlight-result-icon">
|
|
2719
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
2720
|
+
<line x1="3" y1="6" x2="21" y2="6"></line>
|
|
2721
|
+
<line x1="3" y1="12" x2="21" y2="12"></line>
|
|
2722
|
+
<line x1="3" y1="18" x2="21" y2="18"></line>
|
|
2723
|
+
</svg>
|
|
2724
|
+
</div>
|
|
2725
|
+
<div class="spotlight-result-info">
|
|
2726
|
+
<span class="spotlight-result-name">${table}</span>
|
|
2727
|
+
<span class="spotlight-result-schema">public</span>
|
|
2728
|
+
</div>
|
|
2729
|
+
`;
|
|
2730
|
+
|
|
2731
|
+
div.addEventListener('mousemove', () => {
|
|
2732
|
+
setSpotlightSelection(index);
|
|
2733
|
+
});
|
|
2734
|
+
|
|
2735
|
+
div.addEventListener('click', () => {
|
|
2736
|
+
openSpotlightTable(table);
|
|
2737
|
+
});
|
|
2738
|
+
|
|
2739
|
+
spotlightResults.appendChild(div);
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
function navigateSpotlight(direction) {
|
|
2744
|
+
if (spotlightMatches.length === 0) return;
|
|
2745
|
+
|
|
2746
|
+
let newIndex = spotlightSelectedIndex + direction;
|
|
2747
|
+
if (newIndex < 0) newIndex = spotlightMatches.length - 1;
|
|
2748
|
+
if (newIndex >= spotlightMatches.length) newIndex = 0;
|
|
2749
|
+
|
|
2750
|
+
setSpotlightSelection(newIndex);
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
function setSpotlightSelection(index) {
|
|
2754
|
+
spotlightSelectedIndex = index;
|
|
2755
|
+
const items = spotlightResults.querySelectorAll('.spotlight-result-item');
|
|
2756
|
+
items.forEach(item => item.classList.remove('selected'));
|
|
2757
|
+
|
|
2758
|
+
const selectedItem = items[index];
|
|
2759
|
+
if (selectedItem) {
|
|
2760
|
+
selectedItem.classList.add('selected');
|
|
2761
|
+
selectedItem.scrollIntoView({ block: 'nearest' });
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
function executeSpotlightSelection() {
|
|
2766
|
+
if (spotlightSelectedIndex >= 0 && spotlightSelectedIndex < spotlightMatches.length) {
|
|
2767
|
+
openSpotlightTable(spotlightMatches[spotlightSelectedIndex]);
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
function openSpotlightTable(tableName) {
|
|
2772
|
+
toggleSpotlight(false);
|
|
2773
|
+
handleTableSelect(tableName);
|
|
2774
|
+
}
|
package/client/index.html
CHANGED
|
@@ -86,6 +86,42 @@
|
|
|
86
86
|
</div>
|
|
87
87
|
</div>
|
|
88
88
|
|
|
89
|
+
<!-- Schema Dialog -->
|
|
90
|
+
<div class="connection-dialog-overlay" id="schemaDialog" style="display: none;">
|
|
91
|
+
<div class="connection-dialog schema-dialog-content">
|
|
92
|
+
<div class="connection-dialog-header">
|
|
93
|
+
<h2>Table Schema</h2>
|
|
94
|
+
<button class="close-dialog-button" id="closeSchemaDialog">×</button>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="connection-dialog-body schema-body">
|
|
97
|
+
<div class="schema-table-container" id="schemaTableContainer">
|
|
98
|
+
<!-- Schema table rendered here -->
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="connection-dialog-footer">
|
|
102
|
+
<button class="button-primary" id="closeSchemaButton">Close</button>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<!-- Spotlight Search Dialog -->
|
|
108
|
+
<div class="spotlight-overlay" id="spotlightOverlay" style="display: none;">
|
|
109
|
+
<div class="spotlight-modal">
|
|
110
|
+
<div class="spotlight-search-container">
|
|
111
|
+
<svg class="search-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
112
|
+
stroke-width="2">
|
|
113
|
+
<circle cx="11" cy="11" r="8"></circle>
|
|
114
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
115
|
+
</svg>
|
|
116
|
+
<input type="text" id="spotlightInput" placeholder="Search tables..." autocomplete="off">
|
|
117
|
+
<div class="spotlight-shortcut-hint">Cmd + K</div>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="spotlight-results" id="spotlightResults">
|
|
120
|
+
<!-- Results will be rendered here -->
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
89
125
|
<div class="sidebar" id="sidebar">
|
|
90
126
|
<div class="sidebar-header">
|
|
91
127
|
<div class="sidebar-header-top">
|
|
@@ -118,7 +154,7 @@
|
|
|
118
154
|
<h3>Tables <span class="table-count" id="tableCount">0</span></h3>
|
|
119
155
|
</div>
|
|
120
156
|
<div class="sidebar-search-container">
|
|
121
|
-
<input type="text" class="sidebar-search" id="sidebarSearch" placeholder="Search tables..." />
|
|
157
|
+
<input type="text" class="sidebar-search" id="sidebarSearch" placeholder="Search tables (Cmd+K)..." />
|
|
122
158
|
<div class="theme-selector">
|
|
123
159
|
<button class="theme-button" id="themeButton" title="Theme">
|
|
124
160
|
<span class="theme-icon">🌓</span>
|