1// Cache for raw binary catalogs (keyed by exact filename incl. .fits) 2const catalogBinaryCache = window.catalogBinaryCache || (window.catalogBinaryCache = {}); 3// Single-flight guard to avoid duplicate parallel loads for the same catalog 4const catalogBinaryInflight = window.catalogBinaryInflight || (window.catalogBinaryInflight = {}); 5 6function stripCatalogPrefix(name) { 7 return name && name.startsWith('catalogs/') ? name.slice('catalogs/'.length) : name; 8} 9function namesMatch(a, b) { 10 return stripCatalogPrefix(String(a)) === stripCatalogPrefix(String(b)); 11} 12 13 14async function loadBinaryCatalogRawIntoCache(catalogName) { 15 const key = stripCatalogPrefix(catalogName); 16 if (catalogBinaryCache[key]) return catalogBinaryCache[key]; 17 if (catalogBinaryInflight[key]) return catalogBinaryInflight[key]; 18 19 const inflight = (async () => { 20 const metaResp = await apiFetch(`/catalog-metadata/${encodeURIComponent(key)}?ts=${Date.now()}`); 21 if (!metaResp.ok) throw new Error(`Failed to load catalog metadata: ${metaResp.statusText}`); 22 const meta = await metaResp.json(); 23 24 // Only fetch the first page once to warm minimal cache; no multi-page prefetch 25 const url = `/catalog-binary-raw/${encodeURIComponent(key)}?page=1&limit=500&ts=${Date.now()}`; 26 const resp = await apiFetch(url); 27 if (!resp.ok) { 28 let msg = `${resp.status} ${resp.statusText}`; 29 try { msg = (await resp.json()).error || msg; } catch (_) {} 30 throw new Error(`Failed to load raw binary page 1: ${msg}`); 31 } 32 const arrayBuf = await resp.arrayBuffer(); 33 const parsed = (typeof parseBinaryCatalog === 'function') 34 ? parseBinaryCatalog(arrayBuf) 35 : (window.parseBinaryCatalog ? window.parseBinaryCatalog(arrayBuf) : null); 36 if (!parsed || !parsed.records) throw new Error('Binary parse failed (raw)'); 37 38 const columns = meta.column_names || (parsed.records[0] ? Object.keys(parsed.records[0]) : []); 39 const cacheEntry = { 40 header: meta, 41 records: parsed.records, 42 columns, 43 total_rows: meta.total_rows || parsed.records.length, 44 total_columns: columns.length 45 }; 46 catalogBinaryCache[key] = cacheEntry; 47 return cacheEntry; 48 })(); 49 50 catalogBinaryInflight[key] = inflight; 51 try { 52 const result = await inflight; 53 return result; 54 } finally { 55 delete catalogBinaryInflight[key]; 56 } 57 } 58 59 async function ensureBinaryCatalogLoaded(catalogName) { 60 const key = stripCatalogPrefix(catalogName); 61 if (catalogBinaryCache[key]) return catalogBinaryCache[key]; 62 return loadBinaryCatalogRawIntoCache(key); 63 } 64 65 // Metadata for viewer (from RAW cache) 66// Viewer API 67async function fetchCatalogMetadata(catalogName) { 68 const key = stripCatalogPrefix(catalogName); 69 const resp = await apiFetch(`/catalog-metadata/${encodeURIComponent(key)}?ts=${Date.now()}`); 70 if (!resp.ok) { 71 throw new Error(`Failed to load catalog metadata: ${resp.status} ${resp.statusText}`); 72 } 73 const meta = await resp.json(); 74 75 // Normalize columns to the shape this viewer expects 76 let columnsMeta = []; 77 const rawCols = Array.isArray(meta.columns) ? meta.columns : (Array.isArray(meta.column_names) ? meta.column_names : []); 78 if (rawCols.length && typeof rawCols[0] === 'object') { 79 columnsMeta = rawCols; 80 } else { 81 columnsMeta = rawCols.map(name => ({ name, dtype: 'unknown', unit: null, is_numeric: false, is_boolean: false })); 82 } 83 84 const columnNames = columnsMeta.map(c => c.name); 85 const totalRows = meta.total_rows ?? meta.num_records ?? meta.row_count ?? 0; 86 87 return { 88 total_rows: totalRows, 89 total_columns: columnNames.length, 90 column_names: columnNames, 91 columns: columnsMeta, 92 raw: meta 93 }; 94 } 95 96 97 98 99 async function fetchCatalogData(catalogName, options = {}) { 100 const { 101 page = 1, 102 limit = 100, 103 search = null, // client-side (not applied when using raw binary) 104 sortBy = null, // client-side (not applied when using raw binary) 105 sortOrder = 'asc', // client-side (not applied when using raw binary) 106 columns = null, 107 filters = null, // client-side (not applied when using raw binary) 108 stats = false 109 } = options; 110
111 const key = stripCatalogPrefix(catalogName); 112 const hasClientOps = (filters && Object.keys(filters).length > 0) || (search && String(search).trim()) || !!sortBy; 113 114 const colParam = columns ? (Array.isArray(columns) ? columns.join(',') : String(columns)) : ''; 115 const effLimit = hasClientOps ? Math.min(Number(limit)||500, 500) : limit; 116 const url = `/catalog-binary-raw/${encodeURIComponent(key)}?page=${page}&limit=${effLimit}` 117 + (colParam ? `&columns=${encodeURIComponent(colParam)}` : '') 118 + (hasClientOps ? `&filters=${encodeURIComponent(JSON.stringify(filters||{}))}` : '') 119 + (search ? `&search=${encodeURIComponent(String(search))}` : '') 120 + (sortBy ? `&sort_by=${encodeURIComponent(String(sortBy))}&sort_order=${encodeURIComponent(String(sortOrder||'asc'))}` : '') 121 + `&ts=${Date.now()}`; 122 const fetchOpts = {}; 123 if (options && options.signal) fetchOpts.signal = options.signal; 124 const resp = await apiFetch(url, fetchOpts); 125 if (!resp.ok) { 126 let msg = `${resp.status} ${resp.statusText}`; 127 try { msg = (await resp.json()).error || msg; } catch (_) {} 128 throw new Error(`Failed to load data page ${page}: ${msg}`); 129 } 130 131 const arrayBuf = await resp.arrayBuffer(); 132 const parsed = (typeof parseBinaryCatalog === 'function') 133 ? parseBinaryCatalog(arrayBuf) 134 : (window.parseBinaryCatalog ? window.parseBinaryCatalog(arrayBuf) : null); 135 if (!parsed || !parsed.records || !parsed.header) throw new Error('Binary parse failed'); 136 137 const pg = parsed.header.pagination || {}; 138 const totalItems = pg.total_items ?? parsed.header.total_rows ?? 0; 139 const totalPages = pg.total_pages ?? Math.max(1, Math.ceil(totalItems / limit)); 140 const showingStart = pg.showing_start ?? (totalItems ? (page - 1) * limit + 1 : 0); 141 const showingEnd = pg.showing_end ?? Math.min(page * limit, totalItems); 142 143 return { 144 catalog_data: parsed.records, 145 pagination: { 146 page, 147 limit, 148 total_items: totalItems, 149 total_pages: totalPages, 150 has_prev: !!pg.has_prev || page > 1, 151 has_next: !!pg.has_next || page < totalPages, 152 showing_start: showingStart, 153 showing_end: showingEnd 154 }, 155 column_stats: stats ? null : null 156 }; 157 } 158 159 160 161 162// Prefer the already-loaded overlay dataset for this catalog (keeps content consistent) 163function tryGetLocalActiveCatalog(catalogName) { 164 const key = stripCatalogPrefix(catalogName); 165 const active = stripCatalogPrefix(window.currentCatalogName || window.activeCatalog || ''); 166 if (active && active === key && Array.isArray(window.catalogDataWithFlags) && window.catalogDataWithFlags.length > 0) { 167 const columns = 168 (window.catalogMetadata && window.catalogMetadata.column_names) || 169 Object.keys(window.catalogDataWithFlags[0]); 170 return { 171 header: window.catalogMetadata || {}, 172 records: window.catalogDataWithFlags, 173 columns, 174 total_rows: 175 (window.catalogMetadata && (window.catalogMetadata.num_records || window.catalogMetadata.total_rows)) || 176 window.catalogDataWithFlags.length, 177 total_columns: columns.length, 178 }; 179 } 180 return null; 181} 182 183 184 185function showCatalogViewer(catalogName) { 186 showNotification(true, `Loading catalog metadata for ${catalogName}...`); 187 188 // First, get metadata to understand the catalog structure 189 fetchCatalogMetadata(catalogName) 190 .then(metadata => { 191 showNotification(false); 192 createAdvancedCatalogViewer(catalogName, metadata); 193 }) 194 .catch(error => { 195 showNotification(false); 196 showNotification(`Error loading catalog: ${error.message}`, 4000, 'error'); 197 console.error("Error in showCatalogViewer:", error); 198 }); 199} 200 201 202 203 204/** 205 * Create advanced catalog viewer with TopCat-like interface 206 */ 207function createAdvancedCatalogViewer(catalogName, metadata) { 208 const existingPopup = document.getElementById('catalog-viewer-popup'); 209 if (existingPopup) { 210 existingPopup.remove(); 211 } 212 213 const popup = document.createElement('div'); 214 popup.id = 'catalog-viewer-popup'; 215 Object.assign(popup.style, { 216 position: 'fixed', top: '5%', left: '5%', 217 width: '90vw', height: '90vh', backgroundColor: '#2d2d2d', 218 // Keep above Plotter/Region Style (z-index ~3600 range) 219 border: '1px solid #444', borderRadius: '8px', zIndex: '3705', 220 display: 'flex', flexDirection: 'column', 221 boxShadow: '0 8px 16px rgba(0,0,0,0.5)', 222 color: 'white', fontFamily: 'Arial, sans-serif' 223 }); 224 225 // State management 226 const state = { 227 currentPage: 1, 228 pageSize: 500, 229 totalItems: metadata.total_rows, 230 search: '', 231 sortBy: null, 232 sortOrder: 'asc', 233 loading: false, 234 data: [], 235 columns: metadata.column_names, 236 metadata: metadata, 237 activeFilters: {}, 238 // Show all columns by default 239 selectedColumns: Array.isArray(metadata.column_names) ? [...metadata.column_names] : [], 240 selectedRows: new Set() 241 }; 242 243 // Create layout 244 const header = createAdvancedHeader(catalogName, metadata, popup); 245 let loadTimer = null;
246 const scheduleLoad = () => { 247 if (loadTimer) clearTimeout(loadTimer); 248 loadTimer = setTimeout(() => loadData(true), 250); 249 }; 250 const toolbar = createToolbar(state, scheduleLoad); 251 const mainContent = createMainContent(state); 252 const statusBar = createStatusBar(state); 253 254 popup.appendChild(header); 255 popup.appendChild(toolbar); 256 popup.appendChild(mainContent); 257 popup.appendChild(statusBar); 258 document.body.appendChild(popup); 259 260 if (typeof makeDraggable === 'function') { 261 makeDraggable(popup, header); 262 } 263 264 // Load initial data 265 scheduleLoad(); 266 267 async function loadData(immediate) { 268 if (!immediate) return; // Only run when scheduled 269 if (state.loading) { 270 try { if (state._abort && typeof state._abort.abort === 'function') state._abort.abort(); } catch(_){} 271 } 272 273 state.loading = true; 274 // Setup abort controller to cancel older requests 275 try { if (state._abort && typeof state._abort.abort === 'function') state._abort.abort(); } catch(_){} 276 state._abort = (typeof AbortController !== 'undefined') ? new AbortController() : null; 277 updateLoadingState(true); 278 279 try { 280 const options = { 281 page: state.currentPage, 282 limit: state.pageSize, 283 search: state.search || null, 284 sortBy: state.sortBy, 285 sortOrder: state.sortOrder, 286 columns: state.selectedColumns.join(','), 287 filters: Object.keys(state.activeFilters).length > 0 ? state.activeFilters : null, 288 stats: true, 289 signal: state._abort ? state._abort.signal : undefined 290 }; 291 292 const response = await fetchCatalogData(catalogName, options); 293 294 state.data = response.catalog_data; 295 state.totalItems = response.pagination.total_items; 296 297 renderTable(state.data, state.selectedColumns); 298 updatePaginationInfo(response.pagination); 299 updateStatsDisplay(response.column_stats); 300 301 } catch (error) { 302 showNotification(`Error loading data: ${error.message}`, 4000, 'error'); 303 console.error('Error loading catalog data:', error); 304 } finally { 305 state.loading = false; 306 updateLoadingState(false); 307 } 308 } 309 310 function createAdvancedHeader(catalogName, metadata, popup) { 311 const header = document.createElement('div'); 312 Object.assign(header.style, { 313 padding: '10px 15px', borderBottom: '1px solid #444', 314 display: 'flex', justifyContent: 'space-between', alignItems: 'center', 315 backgroundColor: '#333', borderTopLeftRadius: '8px', borderTopRightRadius: '8px', 316 cursor: 'move' 317 }); 318 319 const titleContainer = document.createElement('div'); 320 321 const title = document.createElement('h2'); 322 title.textContent = `Catalog: ${catalogName.split('/').pop()}`; 323 Object.assign(title.style, { margin: '0 0 5px 0', fontSize: '18px', fontWeight: 'bold' }); 324 325 const info = document.createElement('div'); 326 info.innerHTML = ` 327 <span style="color: #aaa; font-size: 12px;"> 328 ${metadata.total_rows.toLocaleString()} rows à ${metadata.total_columns} columns 329 ${metadata.memory_usage_mb ? `⢠${metadata.memory_usage_mb.toFixed(1)} MB` : ''} 330 </span> 331 `; 332 333 titleContainer.appendChild(title); 334 titleContainer.appendChild(info); 335 336 const buttonContainer = document.createElement('div'); 337 Object.assign(buttonContainer.style, { display: 'flex', gap: '10px', alignItems: 'center' }); 338 339 // Info button 340 341 // Export button 342 const exportBtn = createHeaderButton('Export', 'Export', () => showExportDialog(state)); 343 344 // Settings button 345 const settingsBtn = createHeaderButton('Settings', 'Settings', () => showSettingsDialog(state, loadData)); 346 347 const closeButton = document.createElement('button'); 348 closeButton.innerHTML = 'Ã'; 349 Object.assign(closeButton.style, { 350 background: 'none', border: 'none', color: 'white', 351 fontSize: '24px', cursor: 'pointer', fontWeight: 'bold', 352 marginLeft: '10px' 353 }); 354 closeButton.onclick = () => popup.remove(); 355 356 // buttonContainer.appendChild(infoBtn); 357 buttonContainer.appendChild(exportBtn); 358 buttonContainer.appendChild(settingsBtn); 359 buttonContainer.appendChild(closeButton); 360 361 header.appendChild(titleContainer); 362 header.appendChild(buttonContainer); 363 364 return header; 365 } 366 367 function createHeaderButton(text, title, onclick) { 368 const button = document.createElement('button'); 369 button.textContent = text; 370 button.title = title; 371 button.onclick = onclick; 372 Object.assign(button.style, { 373 background: '#444', border: '1px solid #555', color: 'white', 374 fontSize: '14px', cursor: 'pointer', borderRadius: '4px', 375 padding: '5px 8px', height: '32px' 376 }); 377 button.onmouseenter = () => button.style.backgroundColor = '#555'; 378 button.onmouseleave = () => button.style.backgroundColor = '#444'; 379 return button; 380 } 381 382 function createToolbar(state, onUpdate) { 383 const toolbar = document.createElement('div'); 384 Object.assign(toolbar.style, { 385 padding: '10px 15px', 386 display: 'flex', 387 gap: '10px', 388 alignItems: 'center', 389 flexWrap: 'wrap', 390 borderBottom: '1px solid #444', 391 backgroundColor: '#333' 392 }); 393 394 // Search input with advanced options 395 const searchContainer = document.createElement('div'); 396 Object.assign(searchContainer.style, { display: 'flex', alignItems: 'center', gap: '5px' }); 397 398 const searchInput = document.createElement('input'); 399 searchInput.type = 'text'; 400 searchInput.placeholder = 'Search all columns...'; 401 Object.assign(searchInput.style, { 402 minWidth: '200px', 403 padding: '8px', 404 backgroundColor: '#444', 405 color: 'white', 406 border: '1px solid #555', 407 borderRadius: '4px' 408 }); 409 410 const searchButton = document.createElement('button'); 411 searchButton.textContent = 'Advanced'; 412 searchButton.title = 'Advanced Search'; 413 Object.assign(searchButton.style, { 414 padding: '8px', backgroundColor: '#444', color: 'white', 415 border: '1px solid #555', borderRadius: '4px', cursor: 'pointer' 416 }); 417 searchButton.onclick = () => showAdvancedSearchDialog(state, onUpdate); 418 419 searchContainer.appendChild(searchInput); 420 // searchContainer.appendChild(searchButton); 421 422 // Filter indicator 423 const filterIndicator = document.createElement('div'); 424 filterIndicator.className = 'filter-indicator'; 425 Object.assign(filterIndicator.style, { 426 padding: '5px 10px', backgroundColor: '#555', borderRadius: '15px', 427 fontSize: '12px', display: 'none' 428 }); 429 430 // Page size selector 431 const pageSizeSelect = document.createElement('select');
432 [100, 500, 1000, 2000, 5000].forEach(size => { 433 const option = document.createElement('option'); 434 option.value = size; 435 option.textContent = `${size} rows`; 436 option.selected = size === state.pageSize; 437 pageSizeSelect.appendChild(option); 438 }); 439 Object.assign(pageSizeSelect.style, { 440 padding: '8px', backgroundColor: '#444', color: 'white', 441 border: '1px solid #555', borderRadius: '4px' 442 }); 443 444 // Column selector 445 const columnButton = createToolbarButton('Columns', 'Select Columns', () => showColumnSelector(state, onUpdate)); 446 447 // Filter button 448 const filterButton = createToolbarButton('Filters', 'Filters', () => showFilterPanel(state, onUpdate)); 449 450 // Selection info 451 const selectionInfo = document.createElement('div'); 452 selectionInfo.className = 'selection-info'; 453 Object.assign(selectionInfo.style, { 454 fontSize: '12px', color: '#aaa', marginLeft: 'auto' 455 }); 456 457 // Debounced search 458 let searchTimeout; 459 searchInput.addEventListener('input', (e) => { 460 clearTimeout(searchTimeout); 461 searchTimeout = setTimeout(() => { 462 state.search = e.target.value; 463 state.currentPage = 1; 464 onUpdate(); 465 }, 300); 466 }); 467 468 pageSizeSelect.addEventListener('change', (e) => { 469 state.pageSize = parseInt(e.target.value); 470 state.currentPage = 1; 471 onUpdate(); 472 }); 473 474 toolbar.appendChild(searchContainer); 475 toolbar.appendChild(filterIndicator); 476 toolbar.appendChild(pageSizeSelect); 477 // toolbar.appendChild(columnButton); 478 toolbar.appendChild(filterButton); 479 toolbar.appendChild(selectionInfo); 480 481 return toolbar; 482 } 483 484 function createToolbarButton(text, title, onclick) { 485 const button = document.createElement('button'); 486 button.textContent = text; 487 button.title = title; 488 button.onclick = onclick; 489 Object.assign(button.style, { 490 padding: '8px 12px', backgroundColor: '#444', color: 'white', 491 border: '1px solid #555', borderRadius: '4px', cursor: 'pointer' 492 }); 493 button.onmouseenter = () => button.style.backgroundColor = '#555'; 494 button.onmouseleave = () => button.style.backgroundColor = '#444'; 495 return button; 496 } 497 498 function createMainContent(state) { 499 const mainContent = document.createElement('div'); 500 Object.assign(mainContent.style, { 501 display: 'flex', flexGrow: '1', overflow: 'hidden' 502 }); 503 504 // Main table area 505 const tableArea = document.createElement('div'); 506 Object.assign(tableArea.style, { 507 flexGrow: '1', display: 'flex', flexDirection: 'column', 508 overflow: 'hidden' 509 }); 510 511 // Table container 512 const tableContainer = document.createElement('div'); 513 tableContainer.className = 'table-container'; 514 Object.assign(tableContainer.style, { 515 flexGrow: '1', overflow: 'auto', position: 'relative' 516 }); 517 518 const table = document.createElement('table'); 519 table.id = 'catalog-table'; 520 Object.assign(table.style, { 521 width: '100%', borderCollapse: 'collapse' 522 }); 523 524 tableContainer.appendChild(table); 525 526 // Pagination controls 527 const pagination = createPaginationControls(state, scheduleLoad); 528 529 tableArea.appendChild(tableContainer); 530 tableArea.appendChild(pagination); 531 532 mainContent.appendChild(tableArea); 533 534 return mainContent; 535 } 536 537 function createPaginationControls(state, onUpdate) { 538 const pagination = document.createElement('div'); 539 pagination.className = 'pagination-controls'; 540 Object.assign(pagination.style, { 541 padding: '10px 15px', borderTop: '1px solid #444', 542 display: 'flex', justifyContent: 'space-between', alignItems: 'center', 543 backgroundColor: '#333', flexShrink: '0' 544 }); 545 546 // Navigation buttons 547 const navContainer = document.createElement('div'); 548 Object.assign(navContainer.style, { display: 'flex', gap: '5px', alignItems: 'center' }); 549 550 const firstBtn = createPaginationButton('<<', () => { state.currentPage = 1; onUpdate(); }); 551 const prevBtn = createPaginationButton('<', () => { if (state.currentPage > 1) { state.currentPage--; onUpdate(); } }); 552 553 // Page input 554 const pageInput = document.createElement('input'); 555 pageInput.type = 'number'; 556 pageInput.min = '1'; 557 pageInput.value = state.currentPage; 558 Object.assign(pageInput.style, { 559 width: '60px', padding: '5px', backgroundColor: '#444', color: 'white', 560 border: '1px solid #555', borderRadius: '3px', textAlign: 'center' 561 }); 562 pageInput.addEventListener('change', (e) => { 563 const newPage = parseInt(e.target.value);
564 const maxPage = Math.ceil(state.totalItems / state.pageSize); 565 if (newPage >= 1 && newPage <= maxPage) { 566 state.currentPage = newPage; 567 onUpdate(); 568 } 569 }); 570 571 const pageInfo = document.createElement('span'); 572 pageInfo.className = 'page-info'; 573 Object.assign(pageInfo.style, { margin: '0 10px', fontSize: '14px' }); 574 575 const nextBtn = createPaginationButton('>', () => { 576 const maxPage = Math.ceil(state.totalItems / state.pageSize); 577 if (state.currentPage < maxPage) { state.currentPage++; onUpdate(); } 578 }); 579 const lastBtn = createPaginationButton('>>', () => { 580 state.currentPage = Math.ceil(state.totalItems / state.pageSize); 581 onUpdate(); 582 }); 583 584 navContainer.appendChild(firstBtn); 585 navContainer.appendChild(prevBtn); 586 navContainer.appendChild(pageInput); 587 navContainer.appendChild(pageInfo); 588 navContainer.appendChild(nextBtn); 589 navContainer.appendChild(lastBtn); 590 591 // Items info and selection actions 592 const rightContainer = document.createElement('div'); 593 Object.assign(rightContainer.style, { display: 'flex', gap: '15px', alignItems: 'center' }); 594 595 const itemsInfo = document.createElement('span'); 596 itemsInfo.className = 'items-info'; 597 Object.assign(itemsInfo.style, { fontSize: '12px', color: '#aaa' }); 598 599 const selectionActions = document.createElement('div'); 600 selectionActions.className = 'selection-actions'; 601 Object.assign(selectionActions.style, { display: 'none', gap: '5px' }); 602 603 rightContainer.appendChild(itemsInfo); 604 rightContainer.appendChild(selectionActions); 605 606 pagination.appendChild(navContainer); 607 pagination.appendChild(rightContainer); 608 609 return pagination; 610 } 611 612 function createPaginationButton(text, onclick) { 613 const button = document.createElement('button'); 614 button.textContent = text; 615 button.onclick = onclick; 616 Object.assign(button.style, { 617 padding: '5px 10px', backgroundColor: '#444', color: 'white', 618 border: '1px solid #555', borderRadius: '3px', cursor: 'pointer' 619 }); 620 return button; 621 } 622 623 function createStatusBar(state) { 624 const statusBar = document.createElement('div'); 625 statusBar.className = 'status-bar'; 626 Object.assign(statusBar.style, { 627 padding: '5px 15px', borderTop: '1px solid #444', 628 backgroundColor: '#2a2a2a', fontSize: '12px', color: '#aaa', 629 display: 'flex', justifyContent: 'space-between', alignItems: 'center' 630 }); 631 632 const leftStatus = document.createElement('div'); 633 const rightStatus = document.createElement('div'); 634 635 statusBar.appendChild(leftStatus); 636 statusBar.appendChild(rightStatus); 637 638 return statusBar; 639 } 640 641 function renderTable(data, columns) { 642 const table = document.getElementById('catalog-table'); 643 table.innerHTML = ''; 644 645 if (data.length === 0) { 646 const emptyRow = document.createElement('tr'); 647 const emptyCell = document.createElement('td'); 648 emptyCell.textContent = 'No data found'; 649 emptyCell.colSpan = columns.length + 1; 650 Object.assign(emptyCell.style, { 651 textAlign: 'center', padding: '20px', color: '#aaa' 652 }); 653 emptyRow.appendChild(emptyCell); 654 table.appendChild(emptyRow); 655 return; 656 } 657 658 // Create header 659 const thead = document.createElement('thead'); 660 const headerRow = document.createElement('tr'); 661 Object.assign(headerRow.style, { 662 backgroundColor: '#3a3a3a', position: 'sticky', top: '0', zIndex: '1' 663 }); 664 665 // Selection column header 666 const selectTh = document.createElement('th'); 667 selectTh.style.width = '40px'; 668 const selectAllCb = document.createElement('input'); 669 selectAllCb.type = 'checkbox'; 670 selectAllCb.onchange = (e) => toggleSelectAll(e.target.checked); 671 selectTh.appendChild(selectAllCb); 672 headerRow.appendChild(selectTh); 673 674 // Data columns 675 columns.forEach(col => { 676 const th = document.createElement('th'); 677 678 const headerContent = document.createElement('div'); 679 Object.assign(headerContent.style, { 680 display: 'flex', justifyContent: 'space-between', alignItems: 'center' 681 }); 682 683 const colName = document.createElement('span'); 684 colName.textContent = col; 685 colName.style.cursor = 'pointer'; 686 colName.onclick = () => { 687 if (state.sortBy === col) { 688 state.sortOrder = state.sortOrder === 'asc' ? 'desc' : 'asc'; 689 } else { 690 state.sortBy = col; 691 state.sortOrder = 'asc'; 692 } 693 state.currentPage = 1;
694 scheduleLoad(); 695 }; 696 697 const colActions = document.createElement('div'); 698 Object.assign(colActions.style, { display: 'flex', gap: '2px' }); 699 700 // // Column info button 701 // const infoBtn = document.createElement('button'); 702 // infoBtn.textContent = 'Info'; 703 // infoBtn.title = 'Column Info'; 704 // Object.assign(infoBtn.style, { 705 // background: 'none', border: 'none', color: '#aaa', 706 // fontSize: '12px', cursor: 'pointer', padding: '2px' 707 // }); 708 // infoBtn.onclick = (e) => { 709 // e.stopPropagation(); 710 // showColumnInfo(catalogName, col); 711 // }; 712 713 // Filter button 714 const filterBtn = document.createElement('button'); 715 filterBtn.textContent = 'v'; 716 filterBtn.title = 'Filter Column'; 717 Object.assign(filterBtn.style, { 718 background: 'none', border: 'none', color: '#aaa', 719 fontSize: '10px', cursor: 'pointer', padding: '2px' 720 }); 721 filterBtn.onclick = (e) => { 722 e.stopPropagation(); 723 showColumnFilter(col, state, scheduleLoad); 724 }; 725 726 // colActions.appendChild(infoBtn); 727 // colActions.appendChild(filterBtn); 728 729 headerContent.appendChild(colName); 730 headerContent.appendChild(colActions); 731 732 // Add sort indicator 733 if (state.sortBy === col) { 734 const indicator = document.createElement('span'); 735 indicator.textContent = state.sortOrder === 'asc' ? ' ^' : ' v'; 736 colName.appendChild(indicator); 737 } 738 739 Object.assign(th.style, { 740 padding: '10px', textAlign: 'left', userSelect: 'none', 741 borderBottom: '2px solid #555', minWidth: '100px' 742 }); 743 744 th.appendChild(headerContent); 745 headerRow.appendChild(th); 746 }); 747 thead.appendChild(headerRow); 748 table.appendChild(thead); 749 750 // Create body with row selection 751 const tbody = document.createElement('tbody'); 752 data.forEach((row, index) => { 753 const tr = document.createElement('tr'); 754 tr.style.borderBottom = '1px solid #444'; 755 756 // Add hover effect 757 tr.onmouseenter = () => tr.style.backgroundColor = '#383838'; 758 tr.onmouseleave = () => tr.style.backgroundColor = state.selectedRows.has(index) ? '#444' : 'transparent'; 759 760 // Selection checkbox 761 const selectTd = document.createElement('td'); 762 const selectCb = document.createElement('input'); 763 selectCb.type = 'checkbox'; 764 selectCb.checked = state.selectedRows.has(index); 765 selectCb.onchange = (e) => toggleRowSelection(index, e.target.checked); 766 selectTd.appendChild(selectCb); 767 tr.appendChild(selectTd); 768 769 // Data cells 770 columns.forEach(col => { 771 const td = document.createElement('td'); 772 const value = row[col]; 773 774 // Format cell based on data type 775 if (typeof value === 'number') { 776 td.textContent = formatNumber(value); 777 td.style.textAlign = 'right'; 778 } else { 779 td.textContent = value || ''; 780 } 781 782 td.style.padding = '8px 10px'; 783 784 // Add double-click to view cell details 785 td.ondblclick = () => showCellDetails(col, value, row); 786 787 tr.appendChild(td); 788 }); 789 790 tbody.appendChild(tr); 791 }); 792 table.appendChild(tbody); 793 794 updateSelectionInfo(); 795 } 796 797 function toggleSelectAll(checked) { 798 if (checked) { 799 for (let i = 0; i < state.data.length; i++) { 800 state.selectedRows.add(i); 801 } 802 } else { 803 state.selectedRows.clear(); 804 } 805 renderTable(state.data, state.selectedColumns); 806 } 807 808 function toggleRowSelection(index, checked) { 809 if (checked) { 810 state.selectedRows.add(index); 811 } else { 812 state.selectedRows.delete(index); 813 } 814 updateSelectionInfo(); 815 } 816 817 function updateSelectionInfo() { 818 const selectionInfo = document.querySelector('.selection-info'); 819 const selectedCount = state.selectedRows.size; 820 821 if (selectedCount > 0) { 822 selectionInfo.textContent = `${selectedCount} row${selectedCount === 1 ? '' : 's'} selected`; 823 selectionInfo.style.color = '#4CAF50'; 824 } else { 825 selectionInfo.textContent = ''; 826 } 827 828 // Show/hide selection actions 829 const selectionActions = document.querySelector('.selection-actions'); 830 if (selectedCount > 0) { 831 selectionActions.style.display = 'flex'; 832 selectionActions.innerHTML = ` 833 <button onclick="exportSelectedRows()" style="padding: 3px 8px; background: #4CAF50; color: white; border: none; border-radius: 3px; cursor: pointer;">Export Selected</button> 834 <button onclick="clearSelection()" style="padding: 3px 8px; background: #f44336; color: white; border: none; border-radius: 3px; cursor: pointer;">Clear</button> 835 `; 836 } else { 837 selectionActions.style.display = 'none'; 838 } 839 } 840 841 function formatNumber(value) { 842 if (value === null || value === undefined) return ''; 843 if (Math.abs(value) < 0.001 || Math.abs(value) >= 1000000) { 844 return value.toExponential(3); 845 } 846 return value.toFixed(3); 847 } 848 849 function updatePaginationInfo(pagination) { 850 const totalPages = pagination.total_pages; 851 const currentPage = pagination.page; 852 853 // Update page input 854 const pageInput = document.querySelector('.pagination-controls input[type="number"]'); 855 if (pageInput) pageInput.value = currentPage; 856 857 // Update page info 858 const pageInfo = document.querySelector('.page-info'); 859 if (pageInfo) pageInfo.textContent = `of ${totalPages}`; 860 861 // Update items info 862 const itemsInfo = document.querySelector('.items-info'); 863 if (itemsInfo) itemsInfo.textContent = `${pagination.showing_start}-${pagination.showing_end}
863 of ${pagination.total_items.toLocaleString()}`; 864 865 // Update button states 866 const paginationContainer = document.querySelector('.pagination-controls'); 867 if (paginationContainer) { 868 const buttons = paginationContainer.querySelectorAll('button'); 869 if (buttons.length >= 4) { 870 buttons[0].disabled = !pagination.has_prev; // First 871 buttons[1].disabled = !pagination.has_prev; // Previous 872 buttons[2].disabled = !pagination.has_next; // Next 873 buttons[3].disabled = !pagination.has_next; // Last 874 } 875 } 876 877 // Update status bar 878 const statusBar = document.querySelector('.status-bar div:first-child'); 879 if (statusBar) { 880 statusBar.textContent = `Page ${currentPage}/${totalPages} ⢠${pagination.total_items.toLocaleString()} total rows`; 881 } 882 } 883 884 function updateStatsDisplay(stats) { 885 if (!stats) return; 886 887 const statusBar = document.querySelector('.status-bar div:last-child'); 888 if (statusBar) { 889 const numericCols = Object.values(stats).filter(s => s.type === 'numeric').length; 890 const categoricalCols = Object.values(stats).filter(s => s.type === 'categorical').length; 891 statusBar.textContent = `${numericCols} numeric, ${categoricalCols} categorical columns`; 892 } 893 } 894 895 function updateLoadingState(loading) { 896 const table = document.getElementById('catalog-table'); 897 898 if (loading) { 899 // Add loading overlay 900 let loadingOverlay = document.getElementById('loading-overlay'); 901 if (!loadingOverlay) { 902 loadingOverlay = document.createElement('div'); 903 loadingOverlay.id = 'loading-overlay'; 904 Object.assign(loadingOverlay.style, { 905 position: 'absolute', top: '0', left: '0', right: '0', bottom: '0', 906 backgroundColor: 'rgba(45, 45, 45, 0.8)', zIndex: '100', 907 display: 'flex', alignItems: 'center', justifyContent: 'center' 908 }); 909 910 const spinner = document.createElement('div'); 911 Object.assign(spinner.style, { 912 width: '40px', height: '40px', 913 border: '4px solid #555', borderTop: '4px solid #4CAF50', 914 borderRadius: '50%', animation: 'spin 1s linear infinite' 915 }); 916 917 loadingOverlay.appendChild(spinner); 918 table.parentNode.style.position = 'relative'; 919 table.parentNode.appendChild(loadingOverlay); 920 921 // Add animation if not exists 922 if (!document.getElementById('spin-animation')) { 923 const style = document.createElement('style'); 924 style.id = 'spin-animation'; 925 style.textContent = '@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }'; 926 document.head.appendChild(style); 927 } 928 } 929 } else { 930 const loadingOverlay = document.getElementById('loading-overlay'); 931 if (loadingOverlay) { 932 loadingOverlay.remove(); 933 } 934 } 935 } 936 937 938 async function showColumnInfo(catalogName, columnName) { 939 try { 940 showNotification(`Loading column analysis for ${columnName}...`, 2000, 'info'); 941 942 const catalogNameForApi = catalogName.startsWith('catalogs/') 943 ? catalogName.replace('catalogs/', '') 944 : catalogName; 945 946 const response = await fetch(`/catalog-column-analysis/${encodeURIComponent(catalogNameForApi)}/${encodeURIComponent(columnName)}`); 947 const analysis = await response.json(); 948 949 createDialog('column-info-dialog', `Column: ${columnName}`, (content) => { 950 content.innerHTML = ` 951 <div style="padding: 20px; max-height: 500px; overflow-y: auto;"> 952 <h3 style="margin-top: 0; color: #4CAF50;">Basic Information</h3> 953 <table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;"> 954 <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Column:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.column_name}</td></tr> 955 <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Data Type:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.dtype}</td></tr> 956 <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Unit:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.unit || 'None'}</td></tr> 957 <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Total Rows:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">
957${analysis.total_rows.toLocaleString()}</td></tr> 958 <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Sample Size:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.sample_size.toLocaleString()}</td></tr> 959 </table> 960 961 ${analysis.numeric_stats ? ` 962 <h3 style="color: #4CAF50;">Numeric Statistics</h3> 963 <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-bottom: 20px;"> 964 <table style="border-collapse: collapse;"> 965 <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Count:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.numeric_stats.count.toLocaleString()}</td></tr> 966 <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Null:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.numeric_stats.null_count.toLocaleString()}</td></tr> 967 <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Min:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.numeric_stats.min.toFixed(6)}</td></tr> 968 <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Max:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.numeric_stats.max.toFixed(6)}</td></tr> 969 </table> 970 <table style="border-collapse: collapse;"> 971 <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Mean:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.numeric_stats.mean.toFixed(6)}</td></tr> 972 <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Median:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.numeric_stats.median.toFixed(6)}</td></tr> 973 <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Std Dev:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.numeric_stats.std.toFixed(6)}</td></tr> 974 <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Range:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${(analysis.numeric_stats.max - analysis.numeric_stats.min).toFixed(6)}</td></tr> 975 </table> 976 </div> 977 ` : ''} 978 979 ${analysis.categorical_stats ? ` 980 <h3 style="color: #4CAF50;">Categorical Statistics</h3> 981 <table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;"> 982 <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Unique Values:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.categorical_stats.unique_count}</td></tr> 983 <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Diversity Index:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.categorical_stats.diversity_index.toFixed(3)}</td></tr> 984 </table> 985 986 <h4 style="color: #4CAF50;">Most Common Values</h4> 987 <div style="max-height: 200px; overflow-y: auto; border: 1px solid #444; border-radius: 4px;"> 988 <table style="width: 100%; border-collapse: collapse;"> 989 <thead style="position: sticky; top: 0; background: #3a3a3a;"> 990 <tr> 991 <th style="padding: 8px; text-align: left; border-bottom: 1px solid #444;">Value</th> 992 <th style="padding: 8px; text-align: right; border-bottom: 1px solid #444;">Count</th> 993 <th style="padding: 8px; text-align: right; border-bottom: 1px solid #444;">%</th> 994 </tr> 995 </thead> 996 <tbody> 997 ${analysis.categorical_stats.most_common.map(item => ` 998 <tr> 999 <td style="padding: 5px; border-bottom: 1px solid #444; font-family: monospace;">${item.value}</td> 1000 <td style="padding: 5px; border-bottom: 1px solid #444; text-align: right;">${item.count}</td> 1001 <td style="padding: 5px; border-bottom: 1px solid #444; text-align: right;">${((item.count / analysis.sample_size) * 100).toFixed(1)}%</td> 1002 </tr> 1003 `).join('')} 1004 </tbody> 1005 </table> 1006 </div> 1007 ` : ''} 1008 </div> 1009 `; 1010 }); 1011 1012 } catch (error) { 1013 showNotification(`Error loading column analysis: ${error.message}`, 4000, 'error'); 1014 } 1015 } 1016 1017 function showColumnFilter(columnName, state, onUpdate) { 1018 const columnMeta = state.metadata.columns.find(col => col.name === columnName); 1019 const isNumeric = columnMeta && columnMeta.is_numeric; 1020 1021 createDialog('column-filter-dialog', `Filter: ${columnName}`, (content) => { 1022 content.innerHTML = ` 1023 <div style="padding: 20px;"> 1024 <div style="margin-bottom: 15px;"> 1025 <label style="display: block; margin-bottom: 5px; font-weight: bold;">Filter Type:</label> 1026 <select id="filter-type" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;"> 1027 <option value="contains">Contains</option> 1028 <option value="equals">Equals</option> 1029 ${isNumeric ? ` 1030 <option value="greater_than">Greater Than</option> 1031 <option value="less_than">Less Than</option> 1032 <option value="range">Range</option> 1033 ` : ''} 1034 </select> 1035 </div> 1036 1037 <div id="filter-value-container"> 1038 <label style="display: block; margin-bottom: 5px; font-weight: bold;">Value:</label> 1039 <input type="text" id="filter-value" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;"> 1040 </div> 1041 1042 <div id="range-container" style="display: none; margin-top: 15px;"> 1043 <label style="display: block; margin-bottom: 5px; font-weight: bold;">Range:</label> 1044 <div style="display: flex; gap: 10px; align-items: center;"> 1045 <input type="number" id="range-min" placeholder="Min" style="flex: 1; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;">
1046 <span>to</span> 1047 <input type="number" id="range-max" placeholder="Max" style="flex: 1; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;"> 1048 </div> 1049 </div> 1050 1051 <div style="margin-top: 20px; display: flex; gap: 10px; justify-content: flex-end;"> 1052 <button onclick="clearColumnFilter('${columnName}')" style="padding: 8px 16px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer;">Clear Filter</button> 1053 <button onclick="applyColumnFilter('${columnName}')" style="padding: 8px 16px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Apply Filter</button> 1054 </div> 1055 </div> 1056 1057 <script> 1058 document.getElementById('filter-type').addEventListener('change', function() { 1059 const rangeContainer = document.getElementById('range-container'); 1060 const valueContainer = document.getElementById('filter-value-container'); 1061 if (this.value === 'range') { 1062 rangeContainer.style.display = 'block'; 1063 valueContainer.style.display = 'none'; 1064 } else { 1065 rangeContainer.style.display = 'none'; 1066 valueContainer.style.display = 'block'; 1067 } 1068 }); 1069 1070 const existingFilter = ${JSON.stringify(state.activeFilters[columnName] || null)}; 1071 if (existingFilter) { 1072 document.getElementById('filter-type').value = existingFilter.type; 1073 if (existingFilter.type === 'range') { 1074 document.getElementById('range-min').value = existingFilter.min || ''; 1075 document.getElementById('range-max').value = existingFilter.max || ''; 1076 document.getElementById('range-container').style.display = 'block'; 1077 document.getElementById('filter-value-container').style.display = 'none'; 1078 } else { 1079 document.getElementById('filter-value').value = existingFilter.value || ''; 1080 } 1081 } 1082 </script> 1083 `; 1084 }); 1085 1086 window.applyColumnFilter = function(columnName) { 1087 const filterType = document.getElementById('filter-type').value; 1088 1089 if (filterType === 'range') { 1090 const min = document.getElementById('range-min').value; 1091 const max = document.getElementById('range-max').value; 1092 if (min || max) { 1093 state.activeFilters[columnName] = { type: 'range', min: min || null, max: max || null }; 1094 } 1095 } else { 1096 const value = document.getElementById('filter-value').value.trim(); 1097 if (value) { 1098 state.activeFilters[columnName] = { type: filterType, value: value }; 1099 } 1100 } 1101 1102 state.currentPage = 1; 1103 document.getElementById('column-filter-dialog').remove(); 1104 updateFilterIndicator(); 1105 onUpdate(); 1106 }; 1107 1108 window.clearColumnFilter = function(columnName) { 1109 delete state.activeFilters[columnName]; 1110 state.currentPage = 1; 1111 document.getElementById('column-filter-dialog').remove(); 1112 updateFilterIndicator(); 1113 onUpdate(); 1114 }; 1115 } 1116 1117 function showAdvancedSearchDialog(state, onUpdate) { 1118 createDialog('advanced-search-dialog', 'Advanced Search', (content) => { 1119 content.innerHTML = ` 1120 <div style="padding: 20px; width: 600px;"> 1121 <div style="margin-bottom: 15px;"> 1122 <label style="display: block; margin-bottom: 5px; font-weight: bold;">Search Mode:</label> 1123 <select id="search-mode" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;"> 1124 <option value="simple">Simple (contains)</option> 1125 <option value="regex">Regular Expression</option> 1126 <option value="exact">Exact Match</option> 1127 </select> 1128 </div> 1129 1130 <div style="margin-bottom: 15px;"> 1131 <label style="display: block; margin-bottom: 5px; font-weight: bold;">Search Term:</label> 1132 <input type="text" id="search-term" placeholder="Enter search term..." style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;"> 1133 </div> 1134 1135 <div style="margin-bottom: 15px;"> 1136 <label style="display: block; margin-bottom: 5px; font-weight: bold;">Search In Columns:</label> 1137 <div style="max-height: 200px; overflow-y: auto; border: 1px solid #555; border-radius: 4px; padding: 10px; background: #444;"> 1138 ${state.columns.map(col => ` 1139 <label style="display: block; margin-bottom: 5px;"> 1140 <input type="checkbox" value="${col}" checked style="margin-right: 8px;"> 1141 ${col} 1142 </label> 1143 `).join('')} 1144 </div> 1145 </div> 1146 1147 <div style="margin-bottom: 15px;"> 1148 <label style="display: flex; align-items: center;"> 1149 <input type="checkbox" id="case-sensitive" style="margin-right: 8px;"> 1150 Case Sensitive 1151 </label> 1152 </div> 1153 1154 <div style="display: flex; gap: 10px; justify-content: flex-end;"> 1155 <button onclick="document.getElementById('advanced-search-dialog').remove()" style="padding: 8px 16px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer;">Cancel</button> 1156 <button onclick="applyAdvancedSearch()" style="padding: 8px 16px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Search</button> 1157 </div> 1158 </div> 1159 `; 1160 }); 1161 1162 window.applyAdvancedSearch = function() { 1163 const searchTerm = document.getElementById('search-term').value.trim(); 1164 if (searchTerm) { 1165 state.search = searchTerm; 1166 state.currentPage = 1; 1167 onUpdate(); 1168 } 1169 document.getElementById('advanced-search-dialog').remove(); 1170 }; 1171 } 1172 1173 function showColumnSelector(state, onUpdate) { 1174 createDialog('column-selector-dialog', 'Select Columns', (content) => { 1175 content.innerHTML = ` 1176 <div style="padding: 20px;"> 1177 <div style="margin-bottom: 15px; display: flex; gap: 10px;"> 1178 <button id="select-all-btn" style="padding: 6px 12px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Select All</button> 1179 <button id="select-none-btn" style="padding: 6px 12px; background: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer;">Select None</button> 1180 <button id="select-numeric-btn" style="padding: 6px 12px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer;">Numeric Only</button> 1181 </div> 1182 1183 <div style="max-height: 400px; overflow-y: auto; border: 1px solid #555; border-radius: 4px; padding: 10px; background: #444;"> 1184 ${state.metadata.columns.map(col => ` 1185 <label style="display: flex; align-items: center; margin-bottom: 8px; padding: 5px; border-radius: 4px;" 1186 class="column-option" onmouseenter="this.style.background='#555'" onmouseleave="this.style.background='transparent'"> 1187 <input type="checkbox" value="${col.name}" ${state.selectedColumns.includes(col.name) ? 'checked' : ''} 1188 style="margin-right: 10px;" class="column-checkbox"> 1189 <div style="flex: 1;"> 1190 <div style="font-weight: bold; color: ${col.is_numeric ? '#4CAF50' : col.is_boolean ? '#FF9800' : '#2196F3'};"> 1191 ${col.name} 1192 </div> 1193 <div style="font-size: 12px; color: #aaa;"> 1194 ${col.dtype} ${col.unit ? `(${col.unit})` : ''} 1195 </div> 1196 </div> 1197 </label> 1198 `).join('')} 1199 </div> 1200 1201 <div style="margin-top: 15px; padding: 10px; background: #333; border-radius: 4px;"> 1202 <div style="font-size: 12px; color: #aaa;">Selected: <span id="selected-count">0</span> columns</div> 1203 </div> 1204 1205 <div style="margin-top: 20px; display: flex; gap: 10px; justify-content: flex-end;"> 1206 <button onclick="document.getElementById('column-selector-dialog').remove()" style="padding: 8px 16px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer;">Cancel</button> 1207 <button id="apply-selection-btn" style="padding: 8px 16px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Apply</button> 1208 </div> 1209 </div> 1210 `; 1211 1212 // Add event listeners after content is added 1213 setTimeout(() => { 1214 function updateColumnPreview() { 1215 const checked = document.querySelectorAll('.column-checkbox:checked'); 1216 document.getElementById('selected-count').textContent = checked.length; 1217 } 1218 1219 document.getElementById('select-all-btn').onclick = function() { 1220 document.querySelectorAll('.column-checkbox').forEach(cb => cb.checked = true); 1221 updateColumnPreview(); 1222 }; 1223 1224 document.getElementById('select-none-btn').onclick = function() { 1225 document.querySelectorAll('.column-checkbox').forEach(cb => cb.checked = false); 1226 updateColumnPreview(); 1227 }; 1228 1229 document.getElementById('select-numeric-btn').onclick = function() { 1230 document.querySelectorAll('.column-checkbox').forEach(cb => { 1231 const colMeta = state.metadata.columns.find(col => col.name === cb.value); 1232 cb.checked = colMeta && colMeta.is_numeric; 1233 }); 1234 updateColumnPreview(); 1235 }; 1236 1237 document.querySelectorAll('.column-checkbox').forEach(cb => { 1238 cb.addEventListener('change', updateColumnPreview); 1239 }); 1240 1241 document.getElementById('apply-selection-btn').onclick = function() { 1242 const selectedColumns = Array.from(document.querySelectorAll('.column-checkbox:checked')) 1243 .map(cb => cb.value); 1244 1245 if (selectedColumns.length === 0) { 1246 alert('Please select at least one column.'); 1247 return; 1248 } 1249 1250 state.selectedColumns = selectedColumns; 1251 state.currentPage = 1; 1252 document.getElementById('column-selector-dialog').remove(); 1253 onUpdate(); 1254 }; 1255 1256 updateColumnPreview(); 1257 }, 50); 1258 }); 1259 } 1260 1261 function showFilterPanel(state, onUpdate) { 1262 createDialog('filter-panel', 'Advanced Filters', (content) => { 1263 content.innerHTML = ` 1264 <div style="padding: 20px;"> 1265 <div style="margin-bottom: 20px;"> 1266 <h3 style="margin-top: 0; color: #4CAF50;">Active Filters</h3> 1267 <div id="active-filters" style="min-height: 100px; border: 1px solid #555; border-radius: 4px; padding: 10px; background: #444;"> 1268 ${Object.keys(state.activeFilters).length === 0 ? 1269 '<div style="color: #aaa; text-align: center; padding: 20px;">No active filters</div>' : 1270 Object.entries(state.activeFilters).map(([col, filter]) => ` 1271 <div style="background: #333; padding: 8px; margin-bottom: 5px; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;">
1272 <span><strong>${col}:</strong> ${filter.type} "${filter.value || (filter.min || '') + ' - ' + (filter.max || '')}"</span> 1273 <button onclick="removeFilter('${col}')" style="background: #f44336; color: white; border: none; padding: 4px 8px; border-radius: 3px; cursor: pointer;">Ã</button> 1274 </div> 1275 `).join('') 1276 } 1277 </div> 1278 </div> 1279 1280 <div style="margin-bottom: 20px;"> 1281 <h3 style="color: #4CAF50;">Add New Filter</h3> 1282 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px;"> 1283 <div> 1284 <label style="display: block; margin-bottom: 5px;">Column:</label> 1285 <select id="filter-column" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;"> 1286 <option value="">Select column...</option> 1287 ${state.columns.map(col => `<option value="${col}">${col}</option>`).join('')} 1288 </select> 1289 </div> 1290 <div> 1291 <label style="display: block; margin-bottom: 5px;">Operation:</label> 1292 <select id="filter-operation" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;"> 1293 <option value="contains">Contains</option> 1294 <option value="equals">Equals</option> 1295 <option value="greater_than">Greater Than</option> 1296 <option value="less_than">Less Than</option> 1297 <option value="range">Range</option> 1298 </select> 1299 </div> 1300 </div> 1301 1302 <div id="filter-value-input" style="margin-bottom: 10px;"> 1303 <label style="display: block; margin-bottom: 5px;">Value:</label> 1304 <input type="text" id="filter-value" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;"> 1305 </div> 1306 1307 <div id="filter-range-input" style="display: none; margin-bottom: 10px;"> 1308 <label style="display: block; margin-bottom: 5px;">Range:</label> 1309 <div style="display: flex; gap: 10px;"> 1310 <input type="number" id="filter-min" placeholder="Min" style="flex: 1; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;"> 1311 <input type="number" id="filter-max" placeholder="Max" style="flex: 1; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;"> 1312 </div> 1313 </div> 1314 1315 <button id="add-filter-btn" style="width: 100%; padding: 10px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Add Filter</button> 1316 </div> 1317 1318 <div style="display: flex; gap: 10px; justify-content: flex-end;"> 1319 <button id="clear-all-btn" style="padding: 8px 16px; background: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer;">Clear All</button> 1320 <button onclick="document.getElementById('filter-panel').remove()" style="padding: 8px 16px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer;">Close</button> 1321 </div> 1322 </div> 1323 `; 1324 1325 // Add event listeners after content is added 1326 setTimeout(() => { 1327 document.getElementById('filter-operation').addEventListener('change', function() { 1328 const valueInput = document.getElementById('filter-value-input'); 1329 const rangeInput = document.getElementById('filter-range-input'); 1330 if (this.value === 'range') { 1331 valueInput.style.display = 'none'; 1332 rangeInput.style.display = 'block'; 1333 } else { 1334 valueInput.style.display = 'block'; 1335 rangeInput.style.display = 'none'; 1336 } 1337 }); 1338 1339 document.getElementById('add-filter-btn').onclick = function() { 1340 const column = document.getElementById('filter-column').value; 1341 const operation = document.getElementById('filter-operation').value; 1342 1343 if (!column) { 1344 alert('Please select a column'); 1345 return; 1346 } 1347 1348 if (operation === 'range') { 1349 const min = document.getElementById('filter-min').value; 1350 const max = document.getElementById('filter-max').value; 1351 if (!min && !max) { 1352 alert('Please enter min or max value'); 1353 return; 1354 } 1355 state.activeFilters[column] = { type: 'range', min: min || null, max: max || null }; 1356 } else { 1357 const value = document.getElementById('filter-value').value.trim(); 1358 if (!value) { 1359 alert('Please enter a value'); 1360 return; 1361 } 1362 state.activeFilters[column] = { type: operation, value: value }; 1363 } 1364 1365 state.currentPage = 1; 1366 document.getElementById('filter-panel').remove(); 1367 updateFilterIndicator(); 1368 onUpdate(); 1369 }; 1370 1371 document.getElementById('clear-all-btn').onclick = function() { 1372 state.activeFilters = {}; 1373 state.currentPage = 1; 1374 document.getElementById('filter-panel').remove(); 1375 updateFilterIndicator(); 1376 onUpdate(); 1377 }; 1378 1379 window.removeFilter = function(columnName) { 1380 delete state.activeFilters[columnName]; 1381 state.currentPage = 1; 1382 document.getElementById('filter-panel').remove(); 1383 updateFilterIndicator(); 1384 onUpdate(); 1385 }; 1386 }, 50); 1387 }); 1388 } 1389 1390 function showSettingsDialog(state, onUpdate) { 1391 createDialog('settings-dialog', 'Settings', (content) => { 1392 content.innerHTML = ` 1393 <div style="padding: 20px;"> 1394 <div style="margin-bottom: 20px;"> 1395 <h3 style="margin-top: 0; color: #4CAF50;">Display Settings</h3> 1396 1397 <div style="margin-bottom: 15px;"> 1398 <label style="display: block; margin-bottom: 5px;">Decimal Places:</label> 1399 <input type="number" id="decimal-places" min="0" max="10" value="3" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;"> 1400 </div> 1401 1402 <div style="margin-bottom: 15px;"> 1403 <label style="display: flex; align-items: center;"> 1404 <input type="checkbox" id="show-units" checked style="margin-right: 8px;"> 1405 Show Column Units 1406 </label> 1407 </div> 1408 1409 <div style="margin-bottom: 15px;"> 1410 <label style="display: flex; align-items: center;"> 1411 <input type="checkbox" id="highlight-selection" checked style="margin-right: 8px;"> 1412 Highlight Selected Rows 1413 </label> 1414 </div> 1415 </div> 1416 1417 <div style="margin-bottom: 20px;"> 1418 <h3 style="color: #4CAF50;">Performance Settings</h3> 1419 1420 <div style="margin-bottom: 15px;"> 1421 <label style="display: block; margin-bottom: 5px;">Default Page Size:</label> 1422 <select id="default-page-size" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;"> 1423 <option value="100">100 rows</option> 1424 <option value="500">500 rows</option> 1425 <option value="1000" selected>1000 rows</option> 1426 <option value="2000">2000 rows</option> 1427 </select> 1428 </div> 1429 </div> 1430 1431 <div style="margin-bottom: 20px;"> 1432 <h3 style="color: #4CAF50;">Export Settings</h3> 1433 1434 <div style="margin-bottom: 15px;"> 1435 <label style="display: block; margin-bottom: 5px;">Default Export Format:</label> 1436 <select id="export-format" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;"> 1437 <option value="csv">CSV</option> 1438 <option value="tsv">TSV</option> 1439 </select> 1440 </div> 1441 1442 <div style="margin-bottom: 15px;"> 1443 <label style="display: flex; align-items: center;"> 1444 <input type="checkbox" id="include-metadata" checked style="margin-right: 8px;"> 1445 Include Metadata in Export 1446 </label> 1447 </div> 1448 </div> 1449 1450 <div style="display: flex; gap: 10px; justify-content: flex-end;"> 1451 <button onclick="resetSettings()" style="padding: 8px 16px; background: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer;">Reset to Defaults</button> 1452 <button onclick="document.getElementById('settings-dialog').remove()" style="padding: 8px 16px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer;">Cancel</button> 1453 <button onclick="applySettings()" style="padding: 8px 16px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Apply</button> 1454 </div> 1455 </div> 1456 `; 1457 }); 1458 1459 window.applySettings = function() { 1460 showNotification('Settings applied successfully', 2000, 'success'); 1461 document.getElementById('settings-dialog').remove(); 1462 }; 1463 1464 window.resetSettings = function() { 1465 document.getElementById('decimal-places').value = '3'; 1466 document.getElementById('show-units').checked = true; 1467 document.getElementById('highlight-selection').checked = true; 1468 document.getElementById('default-page-size').value = '100'; 1469 document.getElementById('export-format').value = 'csv'; 1470 document.getElementById('include-metadata').checked = true; 1471 }; 1472 } 1473 1474 function showExportDialog(state) { 1475 createDialog('export-dialog', 'Export Data', (content) => { 1476 content.innerHTML = ` 1477 <div style="padding: 20px;"> 1478 <div style="margin-bottom: 20px;"> 1479 <h3 style="margin-top: 0; color: #4CAF50;">Export Options</h3> 1480 1481 <div style="margin-bottom: 15px;"> 1482 <label style="display: block; margin-bottom: 5px; font-weight: bold;">Export Scope:</label> 1483 <div> 1484 <label style="display: block; margin-bottom: 5px;"> 1485 <input type="radio" name="export-scope" value="current" checked style="margin-right: 8px;"> 1486 Current Page (${state.data.length} rows) 1487 </label> 1488 <label style="display: block; margin-bottom: 5px;"> 1489 <input type="radio" name="export-scope" value="filtered" style="margin-right: 8px;"> 1490 All Filtered Data (${state.totalItems.toLocaleStr
1490ing()} rows) 1491 </label> 1492 <label style="display: block; margin-bottom: 5px;"> 1493 <input type="radio" name="export-scope" value="selected" ${state.selectedRows.size === 0 ? 'disabled' : ''} style="margin-right: 8px;"> 1494 Selected Rows (${state.selectedRows.size} rows) 1495 </label> 1496 </div> 1497 </div> 1498 1499 <div style="margin-bottom: 15px;"> 1500 <label style="display: block; margin-bottom: 5px; font-weight: bold;">Export Format:</label> 1501 <select id="export-format" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;"> 1502 <option value="csv">CSV (Comma Separated Values)</option> 1503 <option value="tsv">TSV (Tab Separated Values)</option> 1504 </select> 1505 </div> 1506 1507 <div style="margin-bottom: 15px;"> 1508 <label style="display: block; margin-bottom: 5px; font-weight: bold;">Columns to Export:</label> 1509 <div> 1510 <label style="display: block; margin-bottom: 5px;"> 1511 <input type="radio" name="export-columns" value="visible" checked style="margin-right: 8px;"> 1512 Visible Columns (${state.selectedColumns.length} columns) 1513 </label> 1514 <label style="display: block; margin-bottom: 5px;"> 1515 <input type="radio" name="export-columns" value="all" style="margin-right: 8px;"> 1516 All Columns (${state.columns.length} columns) 1517 </label> 1518 </div> 1519 </div> 1520 1521 <div style="margin-bottom: 15px;"> 1522 <label style="display: flex; align-items: center;"> 1523 <input type="checkbox" id="include-headers" checked style="margin-right: 8px;"> 1524 Include Column Headers 1525 </label> 1526 </div> 1527 </div> 1528 1529 <div style="background: #333; padding: 15px; border-radius: 4px; margin-bottom: 20px;"> 1530 <div style="font-size: 14px; font-weight: bold; margin-bottom: 5px;">Export Preview:</div> 1531 <div id="export-preview" style="font-size: 12px; color: #aaa;"> 1532 Will export current page (${state.data.length} rows) with visible columns (${state.selectedColumns.length} columns) in CSV format 1533 </div> 1534 </div> 1535 1536 <div style="display: flex; gap: 10px; justify-content: flex-end;"> 1537 <button onclick="document.getElementById('export-dialog').remove()" style="padding: 8px 16px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer;">Cancel</button> 1538 <button onclick="performExport()" style="padding: 8px 16px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Export</button> 1539 </div> 1540 </div> 1541 `; 1542 }); 1543 1544 window.performExport = function() { 1545 const scope = document.querySelector('input[name="export-scope"]:checked').value; 1546 const format = document.getElementById('export-format').value; 1547 const columnMode = document.querySelector('input[name="export-columns"]:checked').value; 1548 const includeHeaders = document.getElementById('include-headers').checked; 1549 1550 let exportData = []; 1551 let exportColumns = []; 1552 1553 // Determine data to export 1554 switch(scope) { 1555 case 'current': 1556 exportData = state.data; 1557 break; 1558 case 'selected': 1559 exportData = Array.from(state.selectedRows).map(index => state.data[index]); 1560 break; 1561 case 'filtered': 1562 alert('Exporting all filtered data requires server-side export. This would be implemented with an API call.'); 1563 return; 1564 } 1565 1566 // Determine columns to export 1567 switch(columnMode) { 1568 case 'visible': 1569 exportColumns = state.selectedColumns; 1570 break; 1571 case 'all': 1572 exportColumns = state.columns; 1573 break; 1574 } 1575 1576 // Perform export 1577 try { 1578 let content = ''; 1579 const delimiter = format === 'csv' ? ',' : '\t'; 1580 1581 if (includeHeaders) { 1582 content += exportColumns.join(delimiter) + '\n'; 1583 } 1584 1585 exportData.forEach(row => {
1586 const values = exportColumns.map(col => { 1587 let val = row[col] || ''; 1588 // Escape quotes and wrap in quotes if contains delimiter 1589 if (typeof val === 'string' && (val.includes(delimiter) || val.includes('"') || val.includes('\n'))) { 1590 val = '"' + val.replace(/"/g, '""') + '"'; 1591 } 1592 return val; 1593 }); 1594 content += values.join(delimiter) + '\n'; 1595 }); 1596 1597 // Download file 1598 const blob = new Blob([content], { type: 'text/plain' }); 1599 const url = URL.createObjectURL(blob); 1600 const a = document.createElement('a'); 1601 a.href = url; 1602 a.download = catalogName.split('/').pop().replace('.fits', '') + '_export.' + format; 1603 document.body.appendChild(a); 1604 a.click(); 1605 document.body.removeChild(a); 1606 URL.revokeObjectURL(url); 1607 1608 showNotification('Export completed successfully!', 3000, 'success'); 1609 document.getElementById('export-dialog').remove(); 1610 1611 } catch (error) { 1612 showNotification('Export failed: ' + error.message, 4000, 'error'); 1613 } 1614 }; 1615 } 1616 1617 function showCellDetails(column, value, row) { 1618 createDialog('cell-details-dialog', `Cell Details: ${column}`, (content) => { 1619 content.innerHTML = ` 1620 <div style="padding: 20px;"> 1621 <div style="margin-bottom: 20px;"> 1622 <h3 style="margin-top: 0; color: #4CAF50;">Value Information</h3> 1623 <table style="width: 100%; border-collapse: collapse;"> 1624 <tr><td style="padding: 8px; border-bottom: 1px solid #444; font-weight: bold;">Column:</td><td style="padding: 8px; border-bottom: 1px solid #444; font-family: monospace;">${column}</td></tr> 1625 <tr><td style="padding: 8px; border-bottom: 1px solid #444; font-weight: bold;">Value:</td><td style="padding: 8px; border-bottom: 1px solid #444; font-family: monospace;">${value}</td></tr> 1626 <tr><td style="padding: 8px; border-bottom: 1px solid #444; font-weight: bold;">Type:</td><td style="padding: 8px; border-bottom: 1px solid #444;">${typeof value}</td></tr> 1627 <tr><td style="padding: 8px; border-bottom: 1px solid #444; font-weight: bold;">Length:</td><td style="padding: 8px; border-bottom: 1px solid #444;">${String(value).length} characters</td></tr> 1628 </table> 1629 </div> 1630 1631 <div style="margin-bottom: 20px;"> 1632 <h3 style="color: #4CAF50;">Formatted Views</h3> 1633 <div style="background: #333; padding: 15px; border-radius: 4px;"> 1634 <div style="margin-bottom: 10px;"> 1635 <strong>Raw:</strong> <code style="background: #444; padding: 2px 4px; border-radius: 2px;">${JSON.stringify(value)}</code> 1636 </div> 1637 ${typeof value === 'number' ? ` 1638 <div style="margin-bottom: 10px;"> 1639 <strong>Scientific:</strong> <code style="background: #444; padding: 2px 4px; border-radius: 2px;">${value.toExponential(6)}</code> 1640 </div> 1641 <div style="margin-bottom: 10px;"> 1642 <strong>Fixed:</strong> <code style="background: #444; padding: 2px 4px; border-radius: 2px;">${value.toFixed(6)}</code> 1643 </div> 1644 ` : ''} 1645 <div> 1646 <strong>String:</strong> <code style="background: #444; padding: 2px 4px; border-radius: 2px;">"${String(value)}"</code> 1647 </div> 1648 </div> 1649 </div> 1650 1651 <div style="margin-bottom: 20px;"> 1652 <h3 style="color: #4CAF50;">Row Context</h3> 1653 <div style="max-height: 200px; overflow-y: auto; background: #333; padding: 15px; border-radius: 4px; font-family: monospace; font-size: 12px;"> 1654 <pre style="margin: 0; white-space: pre-wrap;">${JSON.stringify(row, null, 2)}</pre> 1655 </div> 1656 </div> 1657 1658 <div style="display: flex; gap: 10px; justify-content: flex-end;"> 1659 <button onclick="copyCellValue('${String(value).replace(/'/g, "\\'")}', this)" style="padding: 8px 16px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer;">Copy Value</button> 1660 <button onclick="document.getElementById('cell-details-dialog').remove()" style="padding: 8px 16px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer;">Close</button> 1661 </div> 1662 </div> 1663 `; 1664 }); 1665 1666 window.copyCellValue = function(value, button) { 1667 navigator.clipboard.writeText(String(value)).then(() => { 1668 const originalText = button.textContent; 1669 button.textContent = 'Copied!'; 1670 button.style.background = '#4CAF50'; 1671 setTimeout(() => { 1672 button.textContent = originalText; 1673 button.style.background = '#2196F3'; 1674 }, 1000); 1675 }).catch(err => { 1676 showNotification('Failed to copy value', 2000, 'error'); 1677 }); 1678 }; 1679 } 1680 1681 // Utility function to create modal dialogs 1682 function createDialog(id, title, contentCallback) { 1683 // Remove existing dialog if any 1684 const existing = document.getElementById(id); 1685 if (existing) { 1686 existing.remove(); 1687 } 1688 1689 // Create overlay
1690 const overlay = document.createElement('div'); 1691 overlay.id = id; 1692 Object.assign(overlay.style, { 1693 position: 'fixed', top: '0', left: '0', right: '0', bottom: '0', 1694 // Must be above #catalog-viewer-popup (z-index ~3705) 1695 backgroundColor: 'rgba(0, 0, 0, 0.7)', zIndex: '3805', 1696 display: 'flex', alignItems: 'center', justifyContent: 'center' 1697 }); 1698 1699 // Create dialog 1700 const dialog = document.createElement('div'); 1701 Object.assign(dialog.style, { 1702 backgroundColor: '#2d2d2d', borderRadius: '8px', 1703 border: '1px solid #444', maxWidth: '80vw', maxHeight: '80vh', 1704 color: 'white', fontFamily: 'Arial, sans-serif', 1705 boxShadow: '0 8px 32px rgba(0, 0, 0, 0.8)', 1706 overflow: 'hidden', display: 'flex', flexDirection: 'column' 1707 }); 1708 1709 // Create header 1710 const header = document.createElement('div'); 1711 Object.assign(header.style, { 1712 padding: '15px 20px', borderBottom: '1px solid #444', 1713 backgroundColor: '#333', display: 'flex', 1714 justifyContent: 'space-between', alignItems: 'center' 1715 }); 1716 1717 const titleElement = document.createElement('h3'); 1718 titleElement.textContent = title; 1719 titleElement.style.margin = '0'; 1720 1721 const closeButton = document.createElement('button'); 1722 closeButton.innerHTML = 'Ã'; 1723 Object.assign(closeButton.style, { 1724 background: 'none', border: 'none', color: 'white', 1725 fontSize: '24px', cursor: 'pointer', fontWeight: 'bold' 1726 }); 1727 closeButton.onclick = () => overlay.remove(); 1728 1729 header.appendChild(titleElement); 1730 header.appendChild(closeButton); 1731 1732 // Create content container 1733 const content = document.createElement('div'); 1734 Object.assign(content.style, { 1735 overflow: 'auto', flexGrow: '1' 1736 }); 1737 1738 dialog.appendChild(header); 1739 dialog.appendChild(content); 1740 overlay.appendChild(dialog); 1741 1742 // Close on overlay click 1743 overlay.onclick = (e) => { 1744 if (e.target === overlay) { 1745 overlay.remove(); 1746 } 1747 }; 1748 1749 // Close on Escape key 1750 const handleEscape = (e) => { 1751 if (e.key === 'Escape') { 1752 overlay.remove(); 1753 document.removeEventListener('keydown', handleEscape); 1754 } 1755 }; 1756 document.addEventListener('keydown', handleEscape); 1757 1758 document.body.appendChild(overlay); 1759 1760 // Call content callback 1761 contentCallback(content); 1762 1763 return { overlay, dialog, content }; 1764 } 1765 1766 // Update filter indicator 1767 function updateFilterIndicator() { 1768 const filterIndicator = document.querySelector('.filter-indicator'); 1769 if (filterIndicator) { 1770 const activeFilterCount = Object.keys(state.activeFilters).length; 1771 1772 if (activeFilterCount > 0) { 1773 filterIndicator.textContent = `${activeFilterCount} filter${activeFilterCount === 1 ? '' : 's'} active`; 1774 filterIndicator.style.display = 'block'; 1775 } else { 1776 filterIndicator.style.display = 'none'; 1777 } 1778 } 1779 } 1780 1781 // Enhanced notification function 1782 function showNotification(message, duration = 3000, type = 'info') { 1783 // Remove existing notifications 1784 const existing = document.querySelectorAll('.notification'); 1785 existing.forEach(n => n.remove()); 1786 1787 if (message === true || message === false) { 1788 // Handle loading states 1789 if (message === true) { 1790 const loader = document.createElement('div'); 1791 loader.className = 'notification loading'; 1792 loader.innerHTML = ` 1793 <div style="display: flex; align-items: center; gap: 10px;"> 1794 <div style="width: 20px; height: 20px; border: 2px solid #4CAF50; border-top: 2px solid transparent; border-radius: 50%; animation: spin 1s linear infinite;"></div>
1795 <span>Loading...</span> 1796 </div> 1797 `; 1798 Object.assign(loader.style, { 1799 position: 'fixed', top: '20px', right: '20px', zIndex: '4000', 1800 backgroundColor: '#333', color: 'white', padding: '15px 20px', 1801 borderRadius: '6px', border: '1px solid #555', 1802 boxShadow: '0 4px 12px rgba(0,0,0,0.3)', 1803 fontFamily: 'Arial, sans-serif' 1804 }); 1805 1806 // Add spin animation if not exists 1807 if (!document.getElementById('spin-animation')) { 1808 const style = document.createElement('style'); 1809 style.id = 'spin-animation'; 1810 style.textContent = '@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }'; 1811 document.head.appendChild(style); 1812 } 1813 1814 document.body.appendChild(loader); 1815 } 1816 return; 1817 } 1818 1819 const notification = document.createElement('div'); 1820 notification.className = 'notification'; 1821 notification.textContent = message; 1822 1823 const colors = { 1824 info: '#2196F3', 1825 success: '#4CAF50', 1826 warning: '#FF9800', 1827 error: '#f44336' 1828 }; 1829 1830 Object.assign(notification.style, { 1831 position: 'fixed', top: '20px', right: '20px', zIndex: '4000', 1832 backgroundColor: colors[type] || colors.info, color: 'white', 1833 padding: '15px 20px', borderRadius: '6px', 1834 boxShadow: '0 4px 12px rgba(0,0,0,0.3)', 1835 fontFamily: 'Arial, sans-serif', maxWidth: '400px', 1836 transform: 'translateX(100%)', transition: 'transform 0.3s ease' 1837 }); 1838 1839 document.body.appendChild(notification); 1840 1841 // Animate in 1842 setTimeout(() => { 1843 notification.style.transform = 'translateX(0)'; 1844 }, 10); 1845 1846 // Auto remove 1847 if (duration > 0) { 1848 setTimeout(() => { 1849 notification.style.transform = 'translateX(100%)'; 1850 setTimeout(() => { 1851 if (notification.parentNode) { 1852 notification.remove(); 1853 } 1854 }, 300); 1855 }, duration); 1856 } 1857 1858 // Click to dismiss 1859 notification.onclick = () => { 1860 notification.style.transform = 'translateX(100%)'; 1861 setTimeout(() => notification.remove(), 300); 1862 }; 1863 } 1864 1865 // Global functions for selection actions 1866 window.exportSelectedRows = function() { 1867 if (state.selectedRows.size === 0) { 1868 showNotification('No rows selected for export', 2000, 'warning'); 1869 return; 1870 } 1871 1872 showExportDialog(state); 1873 1874 // Set the selected scope after dialog creation 1875 setTimeout(() => { 1876 const selectedRadio = document.querySelector('input[name="export-scope"][value="selected"]'); 1877 if (selectedRadio) { 1878 selectedRadio.checked = true; 1879 } 1880 }, 100); 1881 }; 1882 1883 window.clearSelection = function() { 1884 state.selectedRows.clear(); 1885 renderTable(state.data, state.selectedColumns); 1886 showNotification('Selection cleared', 1500, 'info'); 1887 }; 1888 1889 // Initialize filter indicator 1890 updateFilterIndicator(); 1891}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.