UI Updated
Browse files- index.html +108 -339
index.html
CHANGED
@@ -30,8 +30,8 @@
|
|
30 |
--status-error-bg: #f8d7da;
|
31 |
--status-error-text: #842029;
|
32 |
--status-error-border: #f5c2c7;
|
33 |
-
--loader-border: #f3f3f3;
|
34 |
-
--loader-spinner: #007bff; /*
|
35 |
--input-border: #ced4da;
|
36 |
--input-bg: #fff;
|
37 |
--link-color: #007bff;
|
@@ -39,21 +39,21 @@
|
|
39 |
|
40 |
@media (prefers-color-scheme: dark) {
|
41 |
:root {
|
42 |
-
--bg-color: #1a1a1a;
|
43 |
-
--text-color: #e8e8e8;
|
44 |
-
--header-bg: #2a622d;
|
45 |
--header-text: #e8e8e8;
|
46 |
-
--sidebar-bg: #2c2c2c;
|
47 |
--sidebar-text: #adb5bd;
|
48 |
--sidebar-hover-bg: #444;
|
49 |
-
--sidebar-active-bg: #0056b3;
|
50 |
--sidebar-active-text: white;
|
51 |
-
--content-bg: #212121;
|
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;
|
57 |
--button-primary-hover-bg: #1e7e34;
|
58 |
--button-primary-text: white;
|
59 |
--status-success-bg: #143620;
|
@@ -62,285 +62,84 @@
|
|
62 |
--status-error-bg: #58151c;
|
63 |
--status-error-text: #f5c6cb;
|
64 |
--status-error-border: #842029;
|
65 |
-
--loader-border: #444;
|
66 |
-
--loader-spinner: #4dabf7; /*
|
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 |
-
|
81 |
-
|
82 |
-
|
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;
|
97 |
-
|
98 |
-
|
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 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
}
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
}
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
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;
|
262 |
-
transition: background-color 0.2s;
|
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); }
|
336 |
-
}
|
337 |
</style>
|
338 |
</head>
|
339 |
<body>
|
340 |
|
341 |
<header>
|
342 |
<span>🦆 DuckDB Explorer</span>
|
343 |
-
|
344 |
</header>
|
345 |
|
346 |
<div class="container">
|
@@ -384,15 +183,15 @@
|
|
384 |
</div>
|
385 |
|
386 |
<script>
|
387 |
-
// --- Keep the existing element variables ---
|
388 |
const tableList = document.getElementById('tableList');
|
389 |
const schemaDisplay = document.getElementById('schemaDisplay');
|
390 |
const schemaTable = document.getElementById('schemaTable');
|
391 |
-
const schemaPlaceholder = document.getElementById('schemaPlaceholder');
|
392 |
const dataDisplayContainer = document.getElementById('dataDisplayContainer');
|
393 |
const dataDisplay = document.getElementById('dataDisplay');
|
394 |
const dataTable = document.getElementById('dataTable');
|
395 |
-
const dataPlaceholder = document.getElementById('dataPlaceholder');
|
396 |
const tableDataHeader = document.getElementById('tableDataHeader');
|
397 |
const sqlInput = document.getElementById('sqlInput');
|
398 |
const runSqlButton = document.getElementById('runSqlButton');
|
@@ -400,34 +199,35 @@
|
|
400 |
const queryResultDisplay = document.getElementById('queryResultDisplay');
|
401 |
const queryResultTable = document.getElementById('queryResultTable');
|
402 |
const statusMessage = document.getElementById('statusMessage');
|
403 |
-
const loadingIndicator = document.getElementById('loadingIndicator');
|
404 |
|
405 |
-
// --- API URL
|
406 |
const API_BASE_URL = '';
|
407 |
let currentTables = [];
|
408 |
let selectedTable = null;
|
409 |
let pollingIntervalId = null;
|
410 |
-
const POLLING_INTERVAL_MS =
|
411 |
-
|
412 |
-
// --- Utility Functions
|
413 |
-
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
|
|
428 |
|
429 |
async function fetchAPI(endpoint, options = {}) {
|
430 |
-
showLoader(true);
|
431 |
clearStatus();
|
432 |
const url = `${API_BASE_URL}${endpoint}`;
|
433 |
try {
|
@@ -440,25 +240,24 @@
|
|
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);
|
452 |
showStatus(`Error: ${error.message}`, true);
|
453 |
throw error;
|
454 |
} finally {
|
455 |
-
showLoader(false);
|
456 |
}
|
457 |
}
|
458 |
|
|
|
459 |
function renderTable(data, tableElement) {
|
460 |
tableElement.innerHTML = '';
|
461 |
-
if (!data || !Array.isArray(data)) {
|
462 |
tableElement.innerHTML = '<tbody><tr><td>Invalid data format received.</td></tr></tbody>';
|
463 |
console.error("Invalid data format for renderTable:", data);
|
464 |
return;
|
@@ -485,12 +284,9 @@
|
|
485 |
});
|
486 |
});
|
487 |
}
|
488 |
-
|
489 |
-
|
490 |
-
|
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>';
|
@@ -514,21 +310,21 @@
|
|
514 |
}
|
515 |
|
516 |
|
517 |
-
// --- Event Handlers (
|
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';
|
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;
|
@@ -536,27 +332,19 @@
|
|
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';
|
@@ -565,20 +353,20 @@
|
|
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) {
|
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);
|
576 |
}
|
577 |
}
|
578 |
}
|
579 |
|
580 |
function displayTables(tables) {
|
581 |
-
|
|
|
582 |
if (tables.length === 0) {
|
583 |
tableList.innerHTML = '<li>No tables found.</li>';
|
584 |
return;
|
@@ -588,7 +376,6 @@
|
|
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 |
}
|
@@ -597,6 +384,7 @@
|
|
597 |
}
|
598 |
|
599 |
async function handleTableSelection(listItem) {
|
|
|
600 |
const currentActive = tableList.querySelector('.active');
|
601 |
if (currentActive) {
|
602 |
currentActive.classList.remove('active');
|
@@ -608,10 +396,10 @@
|
|
608 |
|
609 |
queryResultContainer.style.display = 'none';
|
610 |
dataDisplayContainer.style.display = 'flex';
|
611 |
-
dataPlaceholder.style.display = 'none';
|
612 |
|
613 |
tableDataHeader.textContent = `for table "${selectedTable}"`;
|
614 |
-
schemaPlaceholder.style.display = 'none';
|
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 |
|
@@ -630,35 +418,31 @@
|
|
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;
|
645 |
|
646 |
dataDisplayContainer.style.display = 'none';
|
647 |
dataPlaceholder.style.display = 'none';
|
648 |
-
schemaPlaceholder.style.display = 'block';
|
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 |
-
|
656 |
try {
|
657 |
const resultData = await fetchAPI('/query', {
|
658 |
method: 'POST',
|
659 |
-
headers: {
|
660 |
-
'Content-Type': 'application/json',
|
661 |
-
},
|
662 |
body: JSON.stringify({ sql: sql }),
|
663 |
});
|
664 |
renderTable(resultData, queryResultTable);
|
@@ -668,13 +452,12 @@
|
|
668 |
}
|
669 |
}
|
670 |
|
671 |
-
// --- Polling
|
672 |
function startPolling() {
|
673 |
-
stopPolling();
|
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.");
|
@@ -685,28 +468,14 @@
|
|
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 |
|
712 |
</body>
|
|
|
30 |
--status-error-bg: #f8d7da;
|
31 |
--status-error-text: #842029;
|
32 |
--status-error-border: #f5c2c7;
|
33 |
+
/* --loader-border: #f3f3f3; */ /* No longer needed */
|
34 |
+
/* --loader-spinner: #007bff; */ /* No longer needed */
|
35 |
--input-border: #ced4da;
|
36 |
--input-bg: #fff;
|
37 |
--link-color: #007bff;
|
|
|
39 |
|
40 |
@media (prefers-color-scheme: dark) {
|
41 |
:root {
|
42 |
+
--bg-color: #1a1a1a;
|
43 |
+
--text-color: #e8e8e8;
|
44 |
+
--header-bg: #2a622d;
|
45 |
--header-text: #e8e8e8;
|
46 |
+
--sidebar-bg: #2c2c2c;
|
47 |
--sidebar-text: #adb5bd;
|
48 |
--sidebar-hover-bg: #444;
|
49 |
+
--sidebar-active-bg: #0056b3;
|
50 |
--sidebar-active-text: white;
|
51 |
+
--content-bg: #212121;
|
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;
|
57 |
--button-primary-hover-bg: #1e7e34;
|
58 |
--button-primary-text: white;
|
59 |
--status-success-bg: #143620;
|
|
|
62 |
--status-error-bg: #58151c;
|
63 |
--status-error-text: #f5c6cb;
|
64 |
--status-error-border: #842029;
|
65 |
+
/* --loader-border: #444; */ /* No longer needed */
|
66 |
+
/* --loader-spinner: #4dabf7; */ /* No longer needed */
|
67 |
--input-border: #555;
|
68 |
--input-bg: #333;
|
69 |
--link-color: #4dabf7;
|
70 |
}
|
|
|
71 |
input[type="text"], textarea {
|
72 |
color: var(--text-color);
|
73 |
background-color: var(--input-bg);
|
74 |
}
|
|
|
75 |
th, td {
|
76 |
color: var(--text-color);
|
77 |
}
|
78 |
+
::placeholder { color: #888; opacity: 1; }
|
79 |
+
:-ms-input-placeholder { color: #888; }
|
80 |
+
::-ms-input-placeholder { color: #888; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
}
|
82 |
|
83 |
/* --- General Styles --- */
|
84 |
body {
|
85 |
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
86 |
+
margin: 0; padding: 0; display: flex; flex-direction: column; height: 100vh;
|
87 |
+
background-color: var(--bg-color); color: var(--text-color);
|
88 |
+
transition: background-color 0.3s, color 0.3s;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
89 |
}
|
90 |
+
a { color: var(--link-color); }
|
91 |
|
92 |
/* --- Header --- */
|
93 |
header {
|
94 |
+
background-color: var(--header-bg); color: var(--header-text); padding: 15px 20px;
|
95 |
+
display: flex; align-items: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
96 |
+
font-size: 1.2em; font-weight: bold; transition: background-color 0.3s, color 0.3s;
|
97 |
+
}
|
98 |
+
/* Removed header .loader styling */
|
99 |
+
|
100 |
+
/* --- Container, Sidebar, Main Content (Keep as before) --- */
|
101 |
+
.container { display: flex; flex: 1; overflow: hidden; }
|
102 |
+
#sidebar { width: 220px; background-color: var(--sidebar-bg); padding: 15px; overflow-y: auto; border-right: 1px solid var(--border-color); transition: background-color 0.3s; }
|
103 |
+
#sidebar h3 { margin-top: 0; margin-bottom: 15px; color: var(--sidebar-text); border-bottom: 1px solid var(--border-color); padding-bottom: 10px; }
|
104 |
+
#tableList { list-style: none; padding: 0; margin: 0; }
|
105 |
+
#tableList li { padding: 8px 10px; cursor: pointer; border-radius: 4px; margin-bottom: 5px; transition: background-color 0.2s, color 0.2s; font-size: 0.95em; color: var(--sidebar-text); }
|
106 |
+
#tableList li:hover { background-color: var(--sidebar-hover-bg); }
|
107 |
+
#tableList li.active { background-color: var(--sidebar-active-bg); color: var(--sidebar-active-text); font-weight: bold; }
|
108 |
+
#mainContent { flex: 1; display: flex; flex-direction: column; padding: 20px; overflow: hidden; }
|
109 |
+
.content-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; gap: 15px; }
|
110 |
+
#schemaDisplay, #dataDisplayContainer, #queryResultContainer { background-color: var(--content-bg); border: 1px solid var(--border-color); border-radius: 5px; padding: 15px; margin-bottom: 0; overflow: auto; box-shadow: 0 1px 3px rgba(0,0,0,0.05); transition: background-color 0.3s, border-color 0.3s; }
|
111 |
+
#schemaDisplay h4, #dataDisplayContainer h4, #queryResultContainer h4 { margin-top: 0; color: var(--sidebar-text); border-bottom: 1px solid var(--border-color-light); padding-bottom: 10px; margin-bottom: 15px; }
|
112 |
+
#dataDisplayContainer { flex: 1; display: flex; flex-direction: column; }
|
113 |
+
#dataDisplay { flex: 1; overflow: auto; min-height: 100px; }
|
114 |
+
#schemaDisplay p, #dataDisplayContainer p { color: #888; }
|
115 |
+
|
116 |
+
/* --- Query Area (Keep as before) --- */
|
117 |
+
#queryArea { padding-top: 20px; border-top: 1px solid var(--border-color); background-color: var(--content-bg); padding: 15px; border-radius: 5px; box-shadow: 0 -1px 3px rgba(0,0,0,0.05); margin-top: 15px; transition: background-color 0.3s, border-color 0.3s; }
|
118 |
+
#queryArea h4 { margin-top: 0; margin-bottom: 10px; color: var(--sidebar-text); }
|
119 |
+
#queryArea textarea { width: 100%; min-height: 80px; padding: 10px; border: 1px solid var(--input-border); border-radius: 4px; box-sizing: border-box; font-family: monospace; margin-bottom: 10px; resize: vertical; background-color: var(--input-bg); color: var(--text-color); transition: background-color 0.3s, border-color 0.3s, color 0.3s; }
|
120 |
+
#queryArea button { padding: 10px 20px; background-color: var(--button-primary-bg); color: var(--button-primary-text); border: none; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; font-weight: bold; }
|
121 |
+
#queryArea button:hover { background-color: var(--button-primary-hover-bg); }
|
122 |
+
|
123 |
+
/* --- Tables (Keep as before) --- */
|
124 |
+
table { width: 100%; border-collapse: collapse; margin-top: 0; font-size: 0.9em; }
|
125 |
+
th, td { border: 1px solid var(--border-color-light); padding: 10px 12px; text-align: left; white-space: nowrap; max-width: 250px; overflow: hidden; text-overflow: ellipsis; color: var(--text-color); transition: border-color 0.3s; }
|
126 |
+
th { background-color: var(--table-header-bg); font-weight: 600; position: sticky; top: 0; z-index: 1; transition: background-color 0.3s; }
|
127 |
+
tr:nth-child(even) { background-color: var(--table-row-even-bg); transition: background-color 0.3s; }
|
128 |
+
tr:nth-child(odd) { background-color: var(--content-bg); transition: background-color 0.3s; }
|
129 |
+
|
130 |
+
/* --- Status Message (Keep as before) --- */
|
131 |
+
#statusMessage { padding: 10px 15px; margin-top: 15px; border-radius: 4px; display: none; font-size: 0.9em; transition: background-color 0.3s, color 0.3s, border-color 0.3s; }
|
132 |
+
#statusMessage.success { background-color: var(--status-success-bg); color: var(--status-success-text); border: 1px solid var(--status-success-border); }
|
133 |
+
#statusMessage.error { background-color: var(--status-error-bg); color: var(--status-error-text); border: 1px solid var(--status-error-border); }
|
134 |
+
|
135 |
+
/* Removed .loader and @keyframes spin styling */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
136 |
</style>
|
137 |
</head>
|
138 |
<body>
|
139 |
|
140 |
<header>
|
141 |
<span>🦆 DuckDB Explorer</span>
|
142 |
+
<!-- Removed loader div -->
|
143 |
</header>
|
144 |
|
145 |
<div class="container">
|
|
|
183 |
</div>
|
184 |
|
185 |
<script>
|
186 |
+
// --- Keep the existing element variables EXCEPT loadingIndicator ---
|
187 |
const tableList = document.getElementById('tableList');
|
188 |
const schemaDisplay = document.getElementById('schemaDisplay');
|
189 |
const schemaTable = document.getElementById('schemaTable');
|
190 |
+
const schemaPlaceholder = document.getElementById('schemaPlaceholder');
|
191 |
const dataDisplayContainer = document.getElementById('dataDisplayContainer');
|
192 |
const dataDisplay = document.getElementById('dataDisplay');
|
193 |
const dataTable = document.getElementById('dataTable');
|
194 |
+
const dataPlaceholder = document.getElementById('dataPlaceholder');
|
195 |
const tableDataHeader = document.getElementById('tableDataHeader');
|
196 |
const sqlInput = document.getElementById('sqlInput');
|
197 |
const runSqlButton = document.getElementById('runSqlButton');
|
|
|
199 |
const queryResultDisplay = document.getElementById('queryResultDisplay');
|
200 |
const queryResultTable = document.getElementById('queryResultTable');
|
201 |
const statusMessage = document.getElementById('statusMessage');
|
202 |
+
// const loadingIndicator = document.getElementById('loadingIndicator'); // Removed
|
203 |
|
204 |
+
// --- API URL, currentTables, selectedTable, polling (Keep as before) ---
|
205 |
const API_BASE_URL = '';
|
206 |
let currentTables = [];
|
207 |
let selectedTable = null;
|
208 |
let pollingIntervalId = null;
|
209 |
+
const POLLING_INTERVAL_MS = 10000;
|
210 |
+
|
211 |
+
// --- Utility Functions ---
|
212 |
+
// --- REMOVE showLoader function ---
|
213 |
+
// function showLoader(show) {
|
214 |
+
// loadingIndicator.style.display = show ? 'inline-block' : 'none';
|
215 |
+
// }
|
216 |
+
|
217 |
+
// --- Keep showStatus, clearStatus ---
|
218 |
+
function showStatus(message, isError = false) {
|
219 |
+
statusMessage.textContent = message;
|
220 |
+
statusMessage.className = isError ? 'error' : 'success';
|
221 |
+
statusMessage.style.display = 'block';
|
222 |
+
setTimeout(() => { statusMessage.style.display = 'none'; }, 5000);
|
223 |
+
}
|
224 |
+
function clearStatus() {
|
225 |
+
statusMessage.textContent = '';
|
226 |
+
statusMessage.style.display = 'none';
|
227 |
+
}
|
228 |
|
229 |
async function fetchAPI(endpoint, options = {}) {
|
230 |
+
// REMOVE showLoader(true);
|
231 |
clearStatus();
|
232 |
const url = `${API_BASE_URL}${endpoint}`;
|
233 |
try {
|
|
|
240 |
} catch (e) { /* Ignore */ }
|
241 |
throw new Error(errorDetail);
|
242 |
}
|
|
|
243 |
const contentType = response.headers.get("content-type");
|
244 |
if (contentType && contentType.includes("application/json")) {
|
245 |
return await response.json();
|
246 |
}
|
|
|
247 |
return await response.text();
|
248 |
} catch (error) {
|
249 |
console.error('API Fetch Error:', error);
|
250 |
showStatus(`Error: ${error.message}`, true);
|
251 |
throw error;
|
252 |
} finally {
|
253 |
+
// REMOVE showLoader(false);
|
254 |
}
|
255 |
}
|
256 |
|
257 |
+
// --- Keep renderTable, renderSchema ---
|
258 |
function renderTable(data, tableElement) {
|
259 |
tableElement.innerHTML = '';
|
260 |
+
if (!data || !Array.isArray(data)) {
|
261 |
tableElement.innerHTML = '<tbody><tr><td>Invalid data format received.</td></tr></tbody>';
|
262 |
console.error("Invalid data format for renderTable:", data);
|
263 |
return;
|
|
|
284 |
});
|
285 |
});
|
286 |
}
|
287 |
+
function renderSchema(schemaData) {
|
288 |
+
schemaTable.innerHTML = '';
|
289 |
+
schemaPlaceholder.style.display = 'none';
|
|
|
|
|
|
|
290 |
|
291 |
if (!schemaData || !schemaData.columns || schemaData.columns.length === 0) {
|
292 |
schemaTable.innerHTML = '<tbody><tr><td colspan="2">No schema information available.</td></tr></tbody>';
|
|
|
310 |
}
|
311 |
|
312 |
|
313 |
+
// --- Event Handlers (Keep and ensure showLoader calls are removed) ---
|
314 |
|
315 |
async function loadTables(isPolling = false) {
|
316 |
+
// ... (keep existing logic, just remove showLoader calls) ...
|
317 |
if (!isPolling) {
|
318 |
tableList.innerHTML = '<li>Loading tables...</li>';
|
319 |
schemaTable.innerHTML = '';
|
320 |
dataTable.innerHTML = '';
|
321 |
tableDataHeader.textContent = '';
|
322 |
queryResultContainer.style.display = 'none';
|
323 |
+
schemaPlaceholder.style.display = 'block';
|
324 |
dataPlaceholder.style.display = 'block';
|
325 |
}
|
326 |
try {
|
327 |
const newTables = await fetchAPI('/tables');
|
|
|
328 |
if (!isPolling || JSON.stringify(newTables) !== JSON.stringify(currentTables)) {
|
329 |
console.log("Table list changed, updating UI.");
|
330 |
currentTables = newTables;
|
|
|
332 |
if (!isPolling) {
|
333 |
showStatus("Tables loaded.", false);
|
334 |
}
|
|
|
335 |
if (selectedTable && !currentTables.includes(selectedTable)) {
|
336 |
console.log(`Selected table "${selectedTable}" removed.`);
|
337 |
selectedTable = null;
|
338 |
tableDataHeader.textContent = '';
|
339 |
schemaDisplay.innerHTML = '<h4>Schema</h4><p id="schemaPlaceholder">Select a table from the list.</p><table id="schemaTable"></table>';
|
340 |
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>';
|
|
|
341 |
schemaTable = document.getElementById('schemaTable');
|
342 |
dataTable = document.getElementById('dataTable');
|
343 |
schemaPlaceholder = document.getElementById('schemaPlaceholder');
|
344 |
dataPlaceholder = document.getElementById('dataPlaceholder');
|
345 |
tableDataHeader = document.getElementById('tableDataHeader');
|
|
|
|
|
|
|
|
|
346 |
}
|
347 |
}
|
|
|
|
|
348 |
if (currentTables.length > 0 && !isPolling) {
|
349 |
schemaPlaceholder.style.display = 'none';
|
350 |
dataPlaceholder.style.display = 'none';
|
|
|
353 |
schemaPlaceholder.textContent = 'No tables found in the database.';
|
354 |
dataPlaceholder.textContent = 'No tables found in the database.';
|
355 |
}
|
|
|
356 |
} catch (error) {
|
357 |
+
if (!isPolling) {
|
358 |
tableList.innerHTML = '<li>Error loading tables.</li>';
|
359 |
schemaPlaceholder.textContent = 'Error loading tables.';
|
360 |
dataPlaceholder.textContent = 'Error loading tables.';
|
361 |
} else {
|
362 |
+
console.error("Polling error:", error);
|
363 |
}
|
364 |
}
|
365 |
}
|
366 |
|
367 |
function displayTables(tables) {
|
368 |
+
// ... (keep existing logic) ...
|
369 |
+
tableList.innerHTML = '';
|
370 |
if (tables.length === 0) {
|
371 |
tableList.innerHTML = '<li>No tables found.</li>';
|
372 |
return;
|
|
|
376 |
li.textContent = tableName;
|
377 |
li.dataset.tableName = tableName;
|
378 |
li.onclick = () => handleTableSelection(li);
|
|
|
379 |
if (tableName === selectedTable) {
|
380 |
li.classList.add('active');
|
381 |
}
|
|
|
384 |
}
|
385 |
|
386 |
async function handleTableSelection(listItem) {
|
387 |
+
// ... (keep existing logic, just remove showLoader calls) ...
|
388 |
const currentActive = tableList.querySelector('.active');
|
389 |
if (currentActive) {
|
390 |
currentActive.classList.remove('active');
|
|
|
396 |
|
397 |
queryResultContainer.style.display = 'none';
|
398 |
dataDisplayContainer.style.display = 'flex';
|
399 |
+
dataPlaceholder.style.display = 'none';
|
400 |
|
401 |
tableDataHeader.textContent = `for table "${selectedTable}"`;
|
402 |
+
schemaPlaceholder.style.display = 'none';
|
403 |
schemaTable.innerHTML = '<tbody><tr><td colspan="2">Loading schema...</td></tr></tbody>';
|
404 |
dataTable.innerHTML = '<tbody><tr><td>Loading data...</td></tr></tbody>';
|
405 |
|
|
|
418 |
|
419 |
|
420 |
async function runCustomQuery() {
|
421 |
+
// ... (keep existing logic, just remove showLoader calls) ...
|
422 |
const sql = sqlInput.value.trim();
|
423 |
if (!sql) {
|
424 |
showStatus("SQL query cannot be empty.", true);
|
425 |
return;
|
426 |
}
|
|
|
|
|
427 |
const currentActive = tableList.querySelector('.active');
|
428 |
if (currentActive) {
|
429 |
currentActive.classList.remove('active');
|
430 |
}
|
431 |
+
selectedTable = null;
|
432 |
|
433 |
dataDisplayContainer.style.display = 'none';
|
434 |
dataPlaceholder.style.display = 'none';
|
435 |
+
schemaPlaceholder.style.display = 'block';
|
436 |
schemaTable.innerHTML = '';
|
437 |
tableDataHeader.textContent = '';
|
438 |
|
439 |
queryResultContainer.style.display = 'block';
|
440 |
queryResultTable.innerHTML = '<tbody><tr><td>Running query...</td></tr></tbody>';
|
441 |
|
|
|
442 |
try {
|
443 |
const resultData = await fetchAPI('/query', {
|
444 |
method: 'POST',
|
445 |
+
headers: { 'Content-Type': 'application/json', },
|
|
|
|
|
446 |
body: JSON.stringify({ sql: sql }),
|
447 |
});
|
448 |
renderTable(resultData, queryResultTable);
|
|
|
452 |
}
|
453 |
}
|
454 |
|
455 |
+
// --- Keep Polling Functions ---
|
456 |
function startPolling() {
|
457 |
+
stopPolling();
|
458 |
console.log(`Starting polling every ${POLLING_INTERVAL_MS / 1000} seconds.`);
|
459 |
pollingIntervalId = setInterval(() => loadTables(true), POLLING_INTERVAL_MS);
|
460 |
}
|
|
|
461 |
function stopPolling() {
|
462 |
if (pollingIntervalId) {
|
463 |
console.log("Stopping polling.");
|
|
|
468 |
|
469 |
// --- Initial Setup ---
|
470 |
runSqlButton.onclick = runCustomQuery;
|
|
|
|
|
471 |
document.addEventListener('DOMContentLoaded', () => {
|
472 |
loadTables().then(() => {
|
|
|
473 |
if (currentTables.length >= 0) { // Start even if no tables initially
|
474 |
startPolling();
|
475 |
}
|
476 |
});
|
477 |
});
|
478 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
479 |
</script>
|
480 |
|
481 |
</body>
|