amaye15 commited on
Commit
9d7f38f
·
1 Parent(s): 850182e
Files changed (1) hide show
  1. index.html +336 -135
index.html CHANGED
@@ -5,7 +5,92 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>DuckDB Explorer</title>
7
  <style>
8
- /* --- Keep the existing CSS from the previous answer --- */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  body {
10
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
11
  margin: 0;
@@ -13,156 +98,164 @@
13
  display: flex;
14
  flex-direction: column;
15
  height: 100vh;
16
- background-color: #f4f7f6;
17
- color: #333;
 
 
 
 
18
  }
19
 
 
20
  header {
21
- background-color: #4CAF50; /* Changed color slightly */
22
- color: white;
23
- padding: 15px 20px; /* Slightly more padding */
24
  display: flex;
25
  align-items: center;
26
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
27
- font-size: 1.2em; /* Bigger title */
28
  font-weight: bold;
 
29
  }
30
- /* Style for the loader inside the header */
31
  header .loader {
32
- border: 3px solid #f3f3f3; /* Light grey */
33
- border-top: 3px solid #fff; /* White */
34
  border-radius: 50%;
35
  width: 18px;
36
  height: 18px;
37
  animation: spin 1s linear infinite;
38
- display: none; /* Hidden by default */
39
- margin-left: 15px; /* Space from title */
40
  }
41
 
 
42
  .container {
43
  display: flex;
44
  flex: 1;
45
- overflow: hidden; /* Prevent overall container scroll */
46
  }
47
 
 
48
  #sidebar {
49
- width: 220px; /* Slightly wider */
50
- background-color: #e9ecef;
51
  padding: 15px;
52
  overflow-y: auto;
53
- border-right: 1px solid #dee2e6;
 
54
  }
55
-
56
  #sidebar h3 {
57
  margin-top: 0;
58
- margin-bottom: 15px; /* More space */
59
- color: #495057;
60
- border-bottom: 1px solid #ced4da;
61
  padding-bottom: 10px;
62
  }
63
-
64
  #tableList {
65
  list-style: none;
66
  padding: 0;
67
  margin: 0;
68
  }
69
-
70
  #tableList li {
71
- padding: 8px 10px; /* More padding */
72
  cursor: pointer;
73
  border-radius: 4px;
74
  margin-bottom: 5px;
75
- transition: background-color 0.2s, color 0.2s; /* Add color transition */
76
  font-size: 0.95em;
77
- color: #343a40;
78
  }
79
-
80
  #tableList li:hover {
81
- background-color: #d4dadf;
82
  }
83
  #tableList li.active {
84
- background-color: #007bff; /* Bootstrap primary blue */
85
- color: white;
86
  font-weight: bold;
87
  }
88
 
89
-
90
  #mainContent {
91
  flex: 1;
92
  display: flex;
93
  flex-direction: column;
94
  padding: 20px;
95
- overflow: hidden; /* Prevent main content scroll */
96
  }
97
-
98
  .content-area {
99
  flex: 1;
100
  display: flex;
101
  flex-direction: column;
102
- overflow: hidden; /* Child takes scroll */
103
- gap: 15px; /* Add gap between content boxes */
104
  }
105
-
106
  #schemaDisplay, #dataDisplayContainer, #queryResultContainer {
107
- background-color: #fff;
108
- border: 1px solid #dee2e6;
109
- border-radius: 5px; /* Slightly more rounded */
110
  padding: 15px;
111
- margin-bottom: 0; /* Remove margin, use gap instead */
112
- overflow: auto; /* Allow scrolling within these areas */
113
- box-shadow: 0 1px 3px rgba(0,0,0,0.05); /* Subtle shadow */
 
114
  }
115
  #schemaDisplay h4, #dataDisplayContainer h4, #queryResultContainer h4 {
116
  margin-top: 0;
117
- color: #495057;
118
- border-bottom: 1px solid #eee;
119
  padding-bottom: 10px;
120
  margin-bottom: 15px;
121
  }
122
-
123
-
124
  #dataDisplayContainer {
125
- flex: 1; /* Takes remaining space */
126
- display: flex; /* Use flex for inner scroll */
127
  flex-direction: column;
128
  }
129
  #dataDisplay {
130
- flex: 1; /* Allow div itself to scroll */
131
  overflow: auto;
132
- min-height: 100px; /* Ensure it has some height */
133
  }
 
 
 
134
 
135
 
 
136
  #queryArea {
137
  padding-top: 20px;
138
- border-top: 1px solid #dee2e6;
139
- background-color: #f8f9fa; /* Slight background */
140
  padding: 15px;
141
  border-radius: 5px;
142
  box-shadow: 0 -1px 3px rgba(0,0,0,0.05);
 
 
143
  }
144
  #queryArea h4 {
145
  margin-top: 0;
146
  margin-bottom: 10px;
147
- color: #495057;
148
  }
149
-
150
  #queryArea textarea {
151
  width: 100%;
152
  min-height: 80px;
153
  padding: 10px;
154
- border: 1px solid #ced4da; /* Match theme */
155
  border-radius: 4px;
156
  box-sizing: border-box;
157
  font-family: monospace;
158
  margin-bottom: 10px;
159
- resize: vertical; /* Allow vertical resize */
 
 
 
160
  }
161
-
162
  #queryArea button {
163
  padding: 10px 20px;
164
- background-color: #28a745; /* Bootstrap success green */
165
- color: white;
166
  border: none;
167
  border-radius: 4px;
168
  cursor: pointer;
@@ -170,58 +263,73 @@
170
  font-weight: bold;
171
  }
172
  #queryArea button:hover {
173
- background-color: #218838;
174
  }
175
 
 
176
  table {
177
  width: 100%;
178
  border-collapse: collapse;
179
- margin-top: 0; /* Remove margin */
180
- font-size: 0.9em; /* Slightly smaller table font */
181
  }
182
-
183
  th, td {
184
- border: 1px solid #e9ecef; /* Lighter border */
185
- padding: 10px 12px; /* Adjust padding */
186
  text-align: left;
187
  white-space: nowrap;
188
- max-width: 250px; /* Prevent very wide columns */
189
  overflow: hidden;
190
  text-overflow: ellipsis;
 
 
191
  }
192
-
193
  th {
194
- background-color: #f8f9fa; /* Very light header */
195
- font-weight: 600; /* Slightly bolder */
196
- position: sticky; /* Sticky headers */
197
  top: 0;
198
  z-index: 1;
 
199
  }
200
-
201
  tr:nth-child(even) {
202
- background-color: #fdfdfe; /* Very subtle striping */
 
203
  }
 
 
 
 
204
 
 
205
  #statusMessage {
206
  padding: 10px 15px;
207
  margin-top: 15px;
208
  border-radius: 4px;
209
- display: none; /* Hidden by default */
210
  font-size: 0.9em;
 
211
  }
212
-
213
  #statusMessage.success {
214
- background-color: #d1e7dd;
215
- color: #0f5132;
216
- border: 1px solid #badbcc;
217
  }
218
-
219
  #statusMessage.error {
220
- background-color: #f8d7da;
221
- color: #842029;
222
- border: 1px solid #f5c2c7;
223
  }
224
- /* Loader animation */
 
 
 
 
 
 
 
 
 
225
  @keyframes spin {
226
  0% { transform: rotate(0deg); }
227
  100% { transform: rotate(360deg); }
@@ -247,12 +355,12 @@
247
  <div class="content-area">
248
  <div id="schemaDisplay">
249
  <h4>Schema</h4>
250
- <p>Select a table from the list.</p>
251
  <table id="schemaTable"></table>
252
  </div>
253
  <div id="dataDisplayContainer">
254
  <h4>Data <span id="tableDataHeader"></span></h4>
255
- <p>Select a table from the list.</p>
256
  <div id="dataDisplay">
257
  <table id="dataTable"></table>
258
  </div>
@@ -280,9 +388,11 @@
280
  const tableList = document.getElementById('tableList');
281
  const schemaDisplay = document.getElementById('schemaDisplay');
282
  const schemaTable = document.getElementById('schemaTable');
 
283
  const dataDisplayContainer = document.getElementById('dataDisplayContainer');
284
  const dataDisplay = document.getElementById('dataDisplay');
285
  const dataTable = document.getElementById('dataTable');
 
286
  const tableDataHeader = document.getElementById('tableDataHeader');
287
  const sqlInput = document.getElementById('sqlInput');
288
  const runSqlButton = document.getElementById('runSqlButton');
@@ -292,12 +402,14 @@
292
  const statusMessage = document.getElementById('statusMessage');
293
  const loadingIndicator = document.getElementById('loadingIndicator');
294
 
295
- // --- API URL is now relative ---
296
  const API_BASE_URL = '';
297
  let currentTables = [];
298
  let selectedTable = null;
 
 
299
 
300
- // --- Utility Functions (keep existing showLoader, showStatus, clearStatus, renderTable, renderSchema) ---
301
  function showLoader(show) {
302
  loadingIndicator.style.display = show ? 'inline-block' : 'none';
303
  }
@@ -317,7 +429,7 @@
317
  async function fetchAPI(endpoint, options = {}) {
318
  showLoader(true);
319
  clearStatus();
320
- const url = `${API_BASE_URL}${endpoint}`; // API_BASE_URL is now ''
321
  try {
322
  const response = await fetch(url, options);
323
  if (!response.ok) {
@@ -328,10 +440,12 @@
328
  } catch (e) { /* Ignore */ }
329
  throw new Error(errorDetail);
330
  }
331
- if (response.headers.get("content-type")?.includes("application/json")) {
332
- return await response.json();
 
 
333
  }
334
- // Handle potential non-JSON success responses if needed
335
  return await response.text();
336
  } catch (error) {
337
  console.error('API Fetch Error:', error);
@@ -344,7 +458,12 @@
344
 
345
  function renderTable(data, tableElement) {
346
  tableElement.innerHTML = '';
347
- if (!data || data.length === 0) {
 
 
 
 
 
348
  tableElement.innerHTML = '<tbody><tr><td>No data available.</td></tr></tbody>';
349
  return;
350
  }
@@ -362,28 +481,31 @@
362
  headers.forEach(header => {
363
  const cell = row.insertCell();
364
  const value = rowData[header];
365
- // Better null/undefined check and string conversion
366
  cell.textContent = (value === null || value === undefined) ? 'NULL' : String(value);
367
  });
368
  });
369
  }
370
 
 
371
  function renderSchema(schemaData) {
372
- const tableElement = schemaTable;
373
- tableElement.innerHTML = '';
 
 
374
  if (!schemaData || !schemaData.columns || schemaData.columns.length === 0) {
375
- schemaDisplay.innerHTML = '<h4>Schema</h4><p>No schema information available.</p>';
376
  return;
377
  }
378
- schemaDisplay.innerHTML = '<h4>Schema</h4>';
379
- const thead = tableElement.createTHead();
380
  const headerRow = thead.insertRow();
381
  ['Name', 'Type'].forEach(headerText => {
382
  const th = document.createElement('th');
383
  th.textContent = headerText;
384
  headerRow.appendChild(th);
385
  });
386
- const tbody = tableElement.createTBody();
 
387
  schemaData.columns.forEach(column => {
388
  const row = tbody.insertRow();
389
  row.insertCell().textContent = column.name;
@@ -391,35 +513,72 @@
391
  });
392
  }
393
 
 
394
  // --- Event Handlers (Modified) ---
395
 
396
- async function loadTables() {
397
- // No need to get API_BASE_URL from input anymore
398
- tableList.innerHTML = '<li>Loading tables...</li>'; // Indicate loading
399
- schemaTable.innerHTML = ''; // Clear schema
400
- dataTable.innerHTML = ''; // Clear data
401
- tableDataHeader.textContent = '';
402
- queryResultContainer.style.display = 'none';
 
 
 
403
  try {
404
- currentTables = await fetchAPI('/tables');
405
- displayTables(currentTables);
406
- showStatus("Tables loaded.", false);
407
- // Clear placeholder texts
408
- if (currentTables.length > 0) {
409
- schemaDisplay.innerHTML = '<h4>Schema</h4><p>Select a table from the list.</p>';
410
- dataDisplayContainer.querySelector('p').style.display = 'block'; // Show prompt
411
- } else {
412
- schemaDisplay.innerHTML = '<h4>Schema</h4><p>No tables found in the database.</p>';
413
- dataDisplayContainer.querySelector('p').style.display = 'block';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  }
 
 
 
 
 
 
 
 
 
 
 
415
  } catch (error) {
416
- tableList.innerHTML = '<li>Error loading tables.</li>';
 
 
 
 
 
 
417
  }
418
  }
419
 
420
- // --- displayTables and handleTableSelection remain the same ---
421
  function displayTables(tables) {
422
- tableList.innerHTML = ''; // Clear list
423
  if (tables.length === 0) {
424
  tableList.innerHTML = '<li>No tables found.</li>';
425
  return;
@@ -427,8 +586,12 @@
427
  tables.sort().forEach(tableName => {
428
  const li = document.createElement('li');
429
  li.textContent = tableName;
430
- li.dataset.tableName = tableName; // Store table name
431
  li.onclick = () => handleTableSelection(li);
 
 
 
 
432
  tableList.appendChild(li);
433
  });
434
  }
@@ -444,42 +607,49 @@
444
  if (!selectedTable) return;
445
 
446
  queryResultContainer.style.display = 'none';
447
- dataDisplayContainer.style.display = 'flex'; // Make sure it's flex
448
- dataDisplayContainer.querySelector('p').style.display = 'none'; // Hide prompt
449
 
450
  tableDataHeader.textContent = `for table "${selectedTable}"`;
451
- schemaDisplay.innerHTML = '<h4>Schema</h4>'; // Keep header
452
- schemaTable.innerHTML = '<tbody><tr><td>Loading schema...</td></tr></tbody>';
453
- dataTable.innerHTML = '<tbody><tr><td>Loading data...</td></tr></tbody>';
454
 
455
  try {
456
- // Fetch schema and data concurrently
457
  const [schemaResponse, tableDataResponse] = await Promise.all([
458
  fetchAPI(`/tables/${selectedTable}/schema`),
459
- fetchAPI(`/tables/${selectedTable}?limit=100`) // Default limit
460
  ]);
461
  renderSchema(schemaResponse);
462
  renderTable(tableDataResponse, dataTable);
463
  } catch (error) {
464
- // Error already shown by fetchAPI
465
  schemaTable.innerHTML = '<tbody><tr><td colspan="2">Error loading schema.</td></tr></tbody>';
466
  dataTable.innerHTML = '<tbody><tr><td>Error loading data.</td></tr></tbody>';
467
  }
468
  }
469
 
470
 
471
- // --- runCustomQuery remains mostly the same ---
472
  async function runCustomQuery() {
473
  const sql = sqlInput.value.trim();
474
  if (!sql) {
475
  showStatus("SQL query cannot be empty.", true);
476
  return;
477
  }
478
- // No need to check API_BASE_URL anymore
479
 
480
- dataDisplayContainer.style.display = 'none'; // Hide table data
481
- dataDisplayContainer.querySelector('p').style.display = 'none'; // Hide prompt
482
- queryResultContainer.style.display = 'block'; // Show query results area
 
 
 
 
 
 
 
 
 
 
 
483
  queryResultTable.innerHTML = '<tbody><tr><td>Running query...</td></tr></tbody>';
484
 
485
 
@@ -495,16 +665,47 @@
495
  showStatus("Query executed successfully.", false);
496
  } catch (error) {
497
  queryResultTable.innerHTML = '<tbody><tr><td>Error executing query. See status message.</td></tr></tbody>';
498
- // Error is shown by fetchAPI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  }
500
  }
501
 
502
  // --- Initial Setup ---
503
- // Remove connectButton listener
504
  runSqlButton.onclick = runCustomQuery;
505
 
506
- // Load tables automatically when the page loads
507
- document.addEventListener('DOMContentLoaded', loadTables);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
 
509
  </script>
510
 
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>DuckDB Explorer</title>
7
  <style>
8
+ /* --- THEME VARIABLES --- */
9
+ :root {
10
+ --bg-color: #f4f7f6;
11
+ --text-color: #333;
12
+ --header-bg: #4CAF50;
13
+ --header-text: white;
14
+ --sidebar-bg: #e9ecef;
15
+ --sidebar-text: #495057;
16
+ --sidebar-hover-bg: #d4dadf;
17
+ --sidebar-active-bg: #007bff;
18
+ --sidebar-active-text: white;
19
+ --content-bg: #fff;
20
+ --border-color: #dee2e6;
21
+ --border-color-light: #e9ecef;
22
+ --table-header-bg: #f8f9fa;
23
+ --table-row-even-bg: #fdfdfe;
24
+ --button-primary-bg: #28a745;
25
+ --button-primary-hover-bg: #218838;
26
+ --button-primary-text: white;
27
+ --status-success-bg: #d1e7dd;
28
+ --status-success-text: #0f5132;
29
+ --status-success-border: #badbcc;
30
+ --status-error-bg: #f8d7da;
31
+ --status-error-text: #842029;
32
+ --status-error-border: #f5c2c7;
33
+ --loader-border: #f3f3f3;
34
+ --loader-spinner: #007bff; /* Use primary blue for spinner */
35
+ --input-border: #ced4da;
36
+ --input-bg: #fff;
37
+ --link-color: #007bff;
38
+ }
39
+
40
+ @media (prefers-color-scheme: dark) {
41
+ :root {
42
+ --bg-color: #1a1a1a; /* Darker background */
43
+ --text-color: #e8e8e8; /* Lighter text */
44
+ --header-bg: #2a622d; /* Darker header */
45
+ --header-text: #e8e8e8;
46
+ --sidebar-bg: #2c2c2c; /* Dark sidebar */
47
+ --sidebar-text: #adb5bd;
48
+ --sidebar-hover-bg: #444;
49
+ --sidebar-active-bg: #0056b3; /* Darker blue */
50
+ --sidebar-active-text: white;
51
+ --content-bg: #212121; /* Dark content area */
52
+ --border-color: #444;
53
+ --border-color-light: #333;
54
+ --table-header-bg: #2c2c2c;
55
+ --table-row-even-bg: #252525;
56
+ --button-primary-bg: #218838; /* Keep green accessible */
57
+ --button-primary-hover-bg: #1e7e34;
58
+ --button-primary-text: white;
59
+ --status-success-bg: #143620;
60
+ --status-success-text: #a3cfbb;
61
+ --status-success-border: #2a622d;
62
+ --status-error-bg: #58151c;
63
+ --status-error-text: #f5c6cb;
64
+ --status-error-border: #842029;
65
+ --loader-border: #444;
66
+ --loader-spinner: #4dabf7; /* Lighter blue for dark */
67
+ --input-border: #555;
68
+ --input-bg: #333;
69
+ --link-color: #4dabf7;
70
+ }
71
+ /* Ensure inputs inherit text color */
72
+ input[type="text"], textarea {
73
+ color: var(--text-color);
74
+ background-color: var(--input-bg);
75
+ }
76
+ /* Ensure table cells inherit text color */
77
+ th, td {
78
+ color: var(--text-color);
79
+ }
80
+ /* Placeholder text color for dark mode */
81
+ ::placeholder {
82
+ color: #888;
83
+ opacity: 1; /* Firefox */
84
+ }
85
+ :-ms-input-placeholder { /* Internet Explorer 10-11 */
86
+ color: #888;
87
+ }
88
+ ::-ms-input-placeholder { /* Microsoft Edge */
89
+ color: #888;
90
+ }
91
+ }
92
+
93
+ /* --- General Styles --- */
94
  body {
95
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
96
  margin: 0;
 
98
  display: flex;
99
  flex-direction: column;
100
  height: 100vh;
101
+ background-color: var(--bg-color);
102
+ color: var(--text-color);
103
+ transition: background-color 0.3s, color 0.3s; /* Smooth theme transition */
104
+ }
105
+ a {
106
+ color: var(--link-color);
107
  }
108
 
109
+ /* --- Header --- */
110
  header {
111
+ background-color: var(--header-bg);
112
+ color: var(--header-text);
113
+ padding: 15px 20px;
114
  display: flex;
115
  align-items: center;
116
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
117
+ font-size: 1.2em;
118
  font-weight: bold;
119
+ transition: background-color 0.3s, color 0.3s;
120
  }
 
121
  header .loader {
122
+ border: 3px solid var(--loader-border);
123
+ border-top: 3px solid var(--header-text); /* Match header text */
124
  border-radius: 50%;
125
  width: 18px;
126
  height: 18px;
127
  animation: spin 1s linear infinite;
128
+ display: none;
129
+ margin-left: 15px;
130
  }
131
 
132
+ /* --- Container --- */
133
  .container {
134
  display: flex;
135
  flex: 1;
136
+ overflow: hidden;
137
  }
138
 
139
+ /* --- Sidebar --- */
140
  #sidebar {
141
+ width: 220px;
142
+ background-color: var(--sidebar-bg);
143
  padding: 15px;
144
  overflow-y: auto;
145
+ border-right: 1px solid var(--border-color);
146
+ transition: background-color 0.3s;
147
  }
 
148
  #sidebar h3 {
149
  margin-top: 0;
150
+ margin-bottom: 15px;
151
+ color: var(--sidebar-text);
152
+ border-bottom: 1px solid var(--border-color);
153
  padding-bottom: 10px;
154
  }
 
155
  #tableList {
156
  list-style: none;
157
  padding: 0;
158
  margin: 0;
159
  }
 
160
  #tableList li {
161
+ padding: 8px 10px;
162
  cursor: pointer;
163
  border-radius: 4px;
164
  margin-bottom: 5px;
165
+ transition: background-color 0.2s, color 0.2s;
166
  font-size: 0.95em;
167
+ color: var(--sidebar-text); /* Use variable */
168
  }
 
169
  #tableList li:hover {
170
+ background-color: var(--sidebar-hover-bg);
171
  }
172
  #tableList li.active {
173
+ background-color: var(--sidebar-active-bg);
174
+ color: var(--sidebar-active-text);
175
  font-weight: bold;
176
  }
177
 
178
+ /* --- Main Content --- */
179
  #mainContent {
180
  flex: 1;
181
  display: flex;
182
  flex-direction: column;
183
  padding: 20px;
184
+ overflow: hidden;
185
  }
 
186
  .content-area {
187
  flex: 1;
188
  display: flex;
189
  flex-direction: column;
190
+ overflow: hidden;
191
+ gap: 15px;
192
  }
 
193
  #schemaDisplay, #dataDisplayContainer, #queryResultContainer {
194
+ background-color: var(--content-bg);
195
+ border: 1px solid var(--border-color);
196
+ border-radius: 5px;
197
  padding: 15px;
198
+ margin-bottom: 0;
199
+ overflow: auto;
200
+ box-shadow: 0 1px 3px rgba(0,0,0,0.05);
201
+ transition: background-color 0.3s, border-color 0.3s;
202
  }
203
  #schemaDisplay h4, #dataDisplayContainer h4, #queryResultContainer h4 {
204
  margin-top: 0;
205
+ color: var(--sidebar-text); /* Match sidebar heading color */
206
+ border-bottom: 1px solid var(--border-color-light);
207
  padding-bottom: 10px;
208
  margin-bottom: 15px;
209
  }
 
 
210
  #dataDisplayContainer {
211
+ flex: 1;
212
+ display: flex;
213
  flex-direction: column;
214
  }
215
  #dataDisplay {
216
+ flex: 1;
217
  overflow: auto;
218
+ min-height: 100px;
219
  }
220
+ #schemaDisplay p, #dataDisplayContainer p {
221
+ color: #888; /* Placeholder text color */
222
+ }
223
 
224
 
225
+ /* --- Query Area --- */
226
  #queryArea {
227
  padding-top: 20px;
228
+ border-top: 1px solid var(--border-color);
229
+ background-color: var(--content-bg); /* Match content bg */
230
  padding: 15px;
231
  border-radius: 5px;
232
  box-shadow: 0 -1px 3px rgba(0,0,0,0.05);
233
+ margin-top: 15px; /* Add margin instead of relying on flex */
234
+ transition: background-color 0.3s, border-color 0.3s;
235
  }
236
  #queryArea h4 {
237
  margin-top: 0;
238
  margin-bottom: 10px;
239
+ color: var(--sidebar-text);
240
  }
 
241
  #queryArea textarea {
242
  width: 100%;
243
  min-height: 80px;
244
  padding: 10px;
245
+ border: 1px solid var(--input-border);
246
  border-radius: 4px;
247
  box-sizing: border-box;
248
  font-family: monospace;
249
  margin-bottom: 10px;
250
+ resize: vertical;
251
+ background-color: var(--input-bg); /* Input background */
252
+ color: var(--text-color); /* Input text color */
253
+ transition: background-color 0.3s, border-color 0.3s, color 0.3s;
254
  }
 
255
  #queryArea button {
256
  padding: 10px 20px;
257
+ background-color: var(--button-primary-bg);
258
+ color: var(--button-primary-text);
259
  border: none;
260
  border-radius: 4px;
261
  cursor: pointer;
 
263
  font-weight: bold;
264
  }
265
  #queryArea button:hover {
266
+ background-color: var(--button-primary-hover-bg);
267
  }
268
 
269
+ /* --- Tables --- */
270
  table {
271
  width: 100%;
272
  border-collapse: collapse;
273
+ margin-top: 0;
274
+ font-size: 0.9em;
275
  }
 
276
  th, td {
277
+ border: 1px solid var(--border-color-light);
278
+ padding: 10px 12px;
279
  text-align: left;
280
  white-space: nowrap;
281
+ max-width: 250px;
282
  overflow: hidden;
283
  text-overflow: ellipsis;
284
+ color: var(--text-color); /* Inherit text color */
285
+ transition: border-color 0.3s;
286
  }
 
287
  th {
288
+ background-color: var(--table-header-bg);
289
+ font-weight: 600;
290
+ position: sticky;
291
  top: 0;
292
  z-index: 1;
293
+ transition: background-color 0.3s;
294
  }
 
295
  tr:nth-child(even) {
296
+ background-color: var(--table-row-even-bg);
297
+ transition: background-color 0.3s;
298
  }
299
+ tr:nth-child(odd) {
300
+ background-color: var(--content-bg); /* Match content background */
301
+ transition: background-color 0.3s;
302
+ }
303
 
304
+ /* --- Status Message --- */
305
  #statusMessage {
306
  padding: 10px 15px;
307
  margin-top: 15px;
308
  border-radius: 4px;
309
+ display: none;
310
  font-size: 0.9em;
311
+ transition: background-color 0.3s, color 0.3s, border-color 0.3s;
312
  }
 
313
  #statusMessage.success {
314
+ background-color: var(--status-success-bg);
315
+ color: var(--status-success-text);
316
+ border: 1px solid var(--status-success-border);
317
  }
 
318
  #statusMessage.error {
319
+ background-color: var(--status-error-bg);
320
+ color: var(--status-error-text);
321
+ border: 1px solid var(--status-error-border);
322
  }
323
+ /* --- Loader --- */
324
+ .loader {
325
+ border: 4px solid var(--loader-border);
326
+ border-top: 4px solid var(--loader-spinner);
327
+ border-radius: 50%;
328
+ width: 20px;
329
+ height: 20px;
330
+ animation: spin 1s linear infinite;
331
+ display: none;
332
+ }
333
  @keyframes spin {
334
  0% { transform: rotate(0deg); }
335
  100% { transform: rotate(360deg); }
 
355
  <div class="content-area">
356
  <div id="schemaDisplay">
357
  <h4>Schema</h4>
358
+ <p id="schemaPlaceholder">Select a table from the list.</p>
359
  <table id="schemaTable"></table>
360
  </div>
361
  <div id="dataDisplayContainer">
362
  <h4>Data <span id="tableDataHeader"></span></h4>
363
+ <p id="dataPlaceholder">Select a table from the list.</p>
364
  <div id="dataDisplay">
365
  <table id="dataTable"></table>
366
  </div>
 
388
  const tableList = document.getElementById('tableList');
389
  const schemaDisplay = document.getElementById('schemaDisplay');
390
  const schemaTable = document.getElementById('schemaTable');
391
+ const schemaPlaceholder = document.getElementById('schemaPlaceholder'); // Added
392
  const dataDisplayContainer = document.getElementById('dataDisplayContainer');
393
  const dataDisplay = document.getElementById('dataDisplay');
394
  const dataTable = document.getElementById('dataTable');
395
+ const dataPlaceholder = document.getElementById('dataPlaceholder'); // Added
396
  const tableDataHeader = document.getElementById('tableDataHeader');
397
  const sqlInput = document.getElementById('sqlInput');
398
  const runSqlButton = document.getElementById('runSqlButton');
 
402
  const statusMessage = document.getElementById('statusMessage');
403
  const loadingIndicator = document.getElementById('loadingIndicator');
404
 
405
+ // --- API URL is relative ---
406
  const API_BASE_URL = '';
407
  let currentTables = [];
408
  let selectedTable = null;
409
+ let pollingIntervalId = null;
410
+ const POLLING_INTERVAL_MS = 1000; // Refresh table list every 10 seconds
411
 
412
+ // --- Utility Functions (keep existing) ---
413
  function showLoader(show) {
414
  loadingIndicator.style.display = show ? 'inline-block' : 'none';
415
  }
 
429
  async function fetchAPI(endpoint, options = {}) {
430
  showLoader(true);
431
  clearStatus();
432
+ const url = `${API_BASE_URL}${endpoint}`;
433
  try {
434
  const response = await fetch(url, options);
435
  if (!response.ok) {
 
440
  } catch (e) { /* Ignore */ }
441
  throw new Error(errorDetail);
442
  }
443
+ // Check if the response is likely JSON before trying to parse
444
+ const contentType = response.headers.get("content-type");
445
+ if (contentType && contentType.includes("application/json")) {
446
+ return await response.json();
447
  }
448
+ // Assume text for non-JSON or if content-type is missing
449
  return await response.text();
450
  } catch (error) {
451
  console.error('API Fetch Error:', error);
 
458
 
459
  function renderTable(data, tableElement) {
460
  tableElement.innerHTML = '';
461
+ if (!data || !Array.isArray(data)) { // Check if data is an array
462
+ tableElement.innerHTML = '<tbody><tr><td>Invalid data format received.</td></tr></tbody>';
463
+ console.error("Invalid data format for renderTable:", data);
464
+ return;
465
+ }
466
+ if (data.length === 0) {
467
  tableElement.innerHTML = '<tbody><tr><td>No data available.</td></tr></tbody>';
468
  return;
469
  }
 
481
  headers.forEach(header => {
482
  const cell = row.insertCell();
483
  const value = rowData[header];
 
484
  cell.textContent = (value === null || value === undefined) ? 'NULL' : String(value);
485
  });
486
  });
487
  }
488
 
489
+ // --- renderSchema FIX ---
490
  function renderSchema(schemaData) {
491
+ // Target the table directly
492
+ schemaTable.innerHTML = ''; // Clear previous table content
493
+ schemaPlaceholder.style.display = 'none'; // Hide placeholder
494
+
495
  if (!schemaData || !schemaData.columns || schemaData.columns.length === 0) {
496
+ schemaTable.innerHTML = '<tbody><tr><td colspan="2">No schema information available.</td></tr></tbody>';
497
  return;
498
  }
499
+
500
+ const thead = schemaTable.createTHead();
501
  const headerRow = thead.insertRow();
502
  ['Name', 'Type'].forEach(headerText => {
503
  const th = document.createElement('th');
504
  th.textContent = headerText;
505
  headerRow.appendChild(th);
506
  });
507
+
508
+ const tbody = schemaTable.createTBody();
509
  schemaData.columns.forEach(column => {
510
  const row = tbody.insertRow();
511
  row.insertCell().textContent = column.name;
 
513
  });
514
  }
515
 
516
+
517
  // --- Event Handlers (Modified) ---
518
 
519
+ async function loadTables(isPolling = false) {
520
+ if (!isPolling) {
521
+ tableList.innerHTML = '<li>Loading tables...</li>';
522
+ schemaTable.innerHTML = '';
523
+ dataTable.innerHTML = '';
524
+ tableDataHeader.textContent = '';
525
+ queryResultContainer.style.display = 'none';
526
+ schemaPlaceholder.style.display = 'block'; // Show placeholders initially
527
+ dataPlaceholder.style.display = 'block';
528
+ }
529
  try {
530
+ const newTables = await fetchAPI('/tables');
531
+ // Check if the list actually changed before re-rendering
532
+ if (!isPolling || JSON.stringify(newTables) !== JSON.stringify(currentTables)) {
533
+ console.log("Table list changed, updating UI.");
534
+ currentTables = newTables;
535
+ displayTables(currentTables);
536
+ if (!isPolling) {
537
+ showStatus("Tables loaded.", false);
538
+ }
539
+ // If the currently selected table disappeared, clear the displays
540
+ if (selectedTable && !currentTables.includes(selectedTable)) {
541
+ console.log(`Selected table "${selectedTable}" removed.`);
542
+ selectedTable = null;
543
+ tableDataHeader.textContent = '';
544
+ schemaDisplay.innerHTML = '<h4>Schema</h4><p id="schemaPlaceholder">Select a table from the list.</p><table id="schemaTable"></table>';
545
+ dataDisplayContainer.innerHTML = '<h4>Data <span id="tableDataHeader"></span></h4><p id="dataPlaceholder">Select a table from the list.</p><div id="dataDisplay"><table id="dataTable"></table></div>';
546
+ // Re-assign potentially overwritten elements
547
+ schemaTable = document.getElementById('schemaTable');
548
+ dataTable = document.getElementById('dataTable');
549
+ schemaPlaceholder = document.getElementById('schemaPlaceholder');
550
+ dataPlaceholder = document.getElementById('dataPlaceholder');
551
+ tableDataHeader = document.getElementById('tableDataHeader');
552
+ } else if (selectedTable && !isPolling) {
553
+ // Optionally refresh current table view if needed,
554
+ // but might be disruptive. Let's skip for now.
555
+ // handleTableSelection(tableList.querySelector(`li[data-table-name="${selectedTable}"]`));
556
+ }
557
  }
558
+
559
+ // Clear placeholders if tables loaded successfully
560
+ if (currentTables.length > 0 && !isPolling) {
561
+ schemaPlaceholder.style.display = 'none';
562
+ dataPlaceholder.style.display = 'none';
563
+ } else if (currentTables.length === 0 && !isPolling) {
564
+ tableList.innerHTML = '<li>No tables found.</li>';
565
+ schemaPlaceholder.textContent = 'No tables found in the database.';
566
+ dataPlaceholder.textContent = 'No tables found in the database.';
567
+ }
568
+
569
  } catch (error) {
570
+ if (!isPolling) { // Only show initial load error prominently
571
+ tableList.innerHTML = '<li>Error loading tables.</li>';
572
+ schemaPlaceholder.textContent = 'Error loading tables.';
573
+ dataPlaceholder.textContent = 'Error loading tables.';
574
+ } else {
575
+ console.error("Polling error:", error); // Log polling errors quietly
576
+ }
577
  }
578
  }
579
 
 
580
  function displayTables(tables) {
581
+ tableList.innerHTML = '';
582
  if (tables.length === 0) {
583
  tableList.innerHTML = '<li>No tables found.</li>';
584
  return;
 
586
  tables.sort().forEach(tableName => {
587
  const li = document.createElement('li');
588
  li.textContent = tableName;
589
+ li.dataset.tableName = tableName;
590
  li.onclick = () => handleTableSelection(li);
591
+ // Re-apply active class if this table was selected
592
+ if (tableName === selectedTable) {
593
+ li.classList.add('active');
594
+ }
595
  tableList.appendChild(li);
596
  });
597
  }
 
607
  if (!selectedTable) return;
608
 
609
  queryResultContainer.style.display = 'none';
610
+ dataDisplayContainer.style.display = 'flex';
611
+ dataPlaceholder.style.display = 'none'; // Hide placeholder
612
 
613
  tableDataHeader.textContent = `for table "${selectedTable}"`;
614
+ schemaPlaceholder.style.display = 'none'; // Hide placeholder
615
+ schemaTable.innerHTML = '<tbody><tr><td colspan="2">Loading schema...</td></tr></tbody>';
616
+ dataTable.innerHTML = '<tbody><tr><td>Loading data...</td></tr></tbody>';
617
 
618
  try {
 
619
  const [schemaResponse, tableDataResponse] = await Promise.all([
620
  fetchAPI(`/tables/${selectedTable}/schema`),
621
+ fetchAPI(`/tables/${selectedTable}?limit=100`)
622
  ]);
623
  renderSchema(schemaResponse);
624
  renderTable(tableDataResponse, dataTable);
625
  } catch (error) {
 
626
  schemaTable.innerHTML = '<tbody><tr><td colspan="2">Error loading schema.</td></tr></tbody>';
627
  dataTable.innerHTML = '<tbody><tr><td>Error loading data.</td></tr></tbody>';
628
  }
629
  }
630
 
631
 
 
632
  async function runCustomQuery() {
633
  const sql = sqlInput.value.trim();
634
  if (!sql) {
635
  showStatus("SQL query cannot be empty.", true);
636
  return;
637
  }
 
638
 
639
+ // Clear active selection in sidebar if a custom query is run
640
+ const currentActive = tableList.querySelector('.active');
641
+ if (currentActive) {
642
+ currentActive.classList.remove('active');
643
+ }
644
+ selectedTable = null; // Deselect table
645
+
646
+ dataDisplayContainer.style.display = 'none';
647
+ dataPlaceholder.style.display = 'none';
648
+ schemaPlaceholder.style.display = 'block'; // Show placeholders again
649
+ schemaTable.innerHTML = '';
650
+ tableDataHeader.textContent = '';
651
+
652
+ queryResultContainer.style.display = 'block';
653
  queryResultTable.innerHTML = '<tbody><tr><td>Running query...</td></tr></tbody>';
654
 
655
 
 
665
  showStatus("Query executed successfully.", false);
666
  } catch (error) {
667
  queryResultTable.innerHTML = '<tbody><tr><td>Error executing query. See status message.</td></tr></tbody>';
668
+ }
669
+ }
670
+
671
+ // --- Polling Function ---
672
+ function startPolling() {
673
+ stopPolling(); // Clear existing interval if any
674
+ console.log(`Starting polling every ${POLLING_INTERVAL_MS / 1000} seconds.`);
675
+ pollingIntervalId = setInterval(() => loadTables(true), POLLING_INTERVAL_MS);
676
+ }
677
+
678
+ function stopPolling() {
679
+ if (pollingIntervalId) {
680
+ console.log("Stopping polling.");
681
+ clearInterval(pollingIntervalId);
682
+ pollingIntervalId = null;
683
  }
684
  }
685
 
686
  // --- Initial Setup ---
 
687
  runSqlButton.onclick = runCustomQuery;
688
 
689
+ // Load tables automatically when the page loads and start polling
690
+ document.addEventListener('DOMContentLoaded', () => {
691
+ loadTables().then(() => {
692
+ // Start polling only after the initial load succeeds
693
+ if (currentTables.length >= 0) { // Start even if no tables initially
694
+ startPolling();
695
+ }
696
+ });
697
+ });
698
+
699
+ // Optional: Stop polling when the page is hidden (e.g., tab switched)
700
+ // document.addEventListener("visibilitychange", () => {
701
+ // if (document.visibilityState === 'hidden') {
702
+ // stopPolling();
703
+ // } else {
704
+ // // Optionally restart polling immediately or wait for next interval
705
+ // loadTables(); // Refresh immediately when tab becomes visible
706
+ // startPolling();
707
+ // }
708
+ // });
709
 
710
  </script>
711