UI Update
Browse files- 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 |
-
/* ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
17 |
-
color:
|
|
|
|
|
|
|
|
|
18 |
}
|
19 |
|
|
|
20 |
header {
|
21 |
-
background-color:
|
22 |
-
color:
|
23 |
-
padding: 15px 20px;
|
24 |
display: flex;
|
25 |
align-items: center;
|
26 |
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
27 |
-
font-size: 1.2em;
|
28 |
font-weight: bold;
|
|
|
29 |
}
|
30 |
-
/* Style for the loader inside the header */
|
31 |
header .loader {
|
32 |
-
border: 3px solid
|
33 |
-
border-top: 3px solid
|
34 |
border-radius: 50%;
|
35 |
width: 18px;
|
36 |
height: 18px;
|
37 |
animation: spin 1s linear infinite;
|
38 |
-
display: none;
|
39 |
-
margin-left: 15px;
|
40 |
}
|
41 |
|
|
|
42 |
.container {
|
43 |
display: flex;
|
44 |
flex: 1;
|
45 |
-
overflow: hidden;
|
46 |
}
|
47 |
|
|
|
48 |
#sidebar {
|
49 |
-
width: 220px;
|
50 |
-
background-color:
|
51 |
padding: 15px;
|
52 |
overflow-y: auto;
|
53 |
-
border-right: 1px solid
|
|
|
54 |
}
|
55 |
-
|
56 |
#sidebar h3 {
|
57 |
margin-top: 0;
|
58 |
-
margin-bottom: 15px;
|
59 |
-
color:
|
60 |
-
border-bottom: 1px solid
|
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;
|
72 |
cursor: pointer;
|
73 |
border-radius: 4px;
|
74 |
margin-bottom: 5px;
|
75 |
-
transition: background-color 0.2s, color 0.2s;
|
76 |
font-size: 0.95em;
|
77 |
-
color:
|
78 |
}
|
79 |
-
|
80 |
#tableList li:hover {
|
81 |
-
background-color:
|
82 |
}
|
83 |
#tableList li.active {
|
84 |
-
background-color:
|
85 |
-
color:
|
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;
|
96 |
}
|
97 |
-
|
98 |
.content-area {
|
99 |
flex: 1;
|
100 |
display: flex;
|
101 |
flex-direction: column;
|
102 |
-
overflow: hidden;
|
103 |
-
gap: 15px;
|
104 |
}
|
105 |
-
|
106 |
#schemaDisplay, #dataDisplayContainer, #queryResultContainer {
|
107 |
-
background-color:
|
108 |
-
border: 1px solid
|
109 |
-
border-radius: 5px;
|
110 |
padding: 15px;
|
111 |
-
margin-bottom: 0;
|
112 |
-
overflow: auto;
|
113 |
-
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
|
|
114 |
}
|
115 |
#schemaDisplay h4, #dataDisplayContainer h4, #queryResultContainer h4 {
|
116 |
margin-top: 0;
|
117 |
-
color:
|
118 |
-
border-bottom: 1px solid
|
119 |
padding-bottom: 10px;
|
120 |
margin-bottom: 15px;
|
121 |
}
|
122 |
-
|
123 |
-
|
124 |
#dataDisplayContainer {
|
125 |
-
flex: 1;
|
126 |
-
display: flex;
|
127 |
flex-direction: column;
|
128 |
}
|
129 |
#dataDisplay {
|
130 |
-
flex: 1;
|
131 |
overflow: auto;
|
132 |
-
min-height: 100px;
|
133 |
}
|
|
|
|
|
|
|
134 |
|
135 |
|
|
|
136 |
#queryArea {
|
137 |
padding-top: 20px;
|
138 |
-
border-top: 1px solid
|
139 |
-
background-color:
|
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:
|
148 |
}
|
149 |
-
|
150 |
#queryArea textarea {
|
151 |
width: 100%;
|
152 |
min-height: 80px;
|
153 |
padding: 10px;
|
154 |
-
border: 1px solid
|
155 |
border-radius: 4px;
|
156 |
box-sizing: border-box;
|
157 |
font-family: monospace;
|
158 |
margin-bottom: 10px;
|
159 |
-
resize: vertical;
|
|
|
|
|
|
|
160 |
}
|
161 |
-
|
162 |
#queryArea button {
|
163 |
padding: 10px 20px;
|
164 |
-
background-color:
|
165 |
-
color:
|
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:
|
174 |
}
|
175 |
|
|
|
176 |
table {
|
177 |
width: 100%;
|
178 |
border-collapse: collapse;
|
179 |
-
margin-top: 0;
|
180 |
-
font-size: 0.9em;
|
181 |
}
|
182 |
-
|
183 |
th, td {
|
184 |
-
border: 1px solid
|
185 |
-
padding: 10px 12px;
|
186 |
text-align: left;
|
187 |
white-space: nowrap;
|
188 |
-
max-width: 250px;
|
189 |
overflow: hidden;
|
190 |
text-overflow: ellipsis;
|
|
|
|
|
191 |
}
|
192 |
-
|
193 |
th {
|
194 |
-
background-color:
|
195 |
-
font-weight: 600;
|
196 |
-
position: sticky;
|
197 |
top: 0;
|
198 |
z-index: 1;
|
|
|
199 |
}
|
200 |
-
|
201 |
tr:nth-child(even) {
|
202 |
-
background-color:
|
|
|
203 |
}
|
|
|
|
|
|
|
|
|
204 |
|
|
|
205 |
#statusMessage {
|
206 |
padding: 10px 15px;
|
207 |
margin-top: 15px;
|
208 |
border-radius: 4px;
|
209 |
-
display: none;
|
210 |
font-size: 0.9em;
|
|
|
211 |
}
|
212 |
-
|
213 |
#statusMessage.success {
|
214 |
-
background-color:
|
215 |
-
color:
|
216 |
-
border: 1px solid
|
217 |
}
|
218 |
-
|
219 |
#statusMessage.error {
|
220 |
-
background-color:
|
221 |
-
color:
|
222 |
-
border: 1px solid
|
223 |
}
|
224 |
-
/* Loader
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
296 |
const API_BASE_URL = '';
|
297 |
let currentTables = [];
|
298 |
let selectedTable = null;
|
|
|
|
|
299 |
|
300 |
-
// --- Utility Functions (keep existing
|
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}`;
|
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
|
332 |
-
|
|
|
|
|
333 |
}
|
334 |
-
//
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
373 |
-
|
|
|
|
|
374 |
if (!schemaData || !schemaData.columns || schemaData.columns.length === 0) {
|
375 |
-
|
376 |
return;
|
377 |
}
|
378 |
-
|
379 |
-
const thead =
|
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 |
-
|
|
|
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 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
|
|
|
|
|
|
403 |
try {
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
414 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
415 |
} catch (error) {
|
416 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
417 |
}
|
418 |
}
|
419 |
|
420 |
-
// --- displayTables and handleTableSelection remain the same ---
|
421 |
function displayTables(tables) {
|
422 |
-
tableList.innerHTML = '';
|
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;
|
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';
|
448 |
-
|
449 |
|
450 |
tableDataHeader.textContent = `for table "${selectedTable}"`;
|
451 |
-
|
452 |
-
|
453 |
-
|
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`)
|
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 |
-
|
481 |
-
|
482 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|