diff --git a/webui/web/explorer.html b/webui/web/explorer.html index b2bc6a37..c06f7bdd 100644 --- a/webui/web/explorer.html +++ b/webui/web/explorer.html @@ -285,6 +285,42 @@ under the License. + + @@ -1085,6 +1121,13 @@ under the License. let currentObjectKey = null; // For info modal let searchTerm = ''; // Search filter + // Pagination state + let currentPage = 1; + let pageSize = 10; + let continuationTokens = [null]; // continuationTokens[i] = S3 token needed to fetch page i+1 + let isTruncated = false; + let isReloadingFirstPageForSearch = false; + // Versioning state let showVersions = false; let currentBucketVersioningStatus = null; @@ -1528,15 +1571,23 @@ under the License. // Object Loading // ============================================ - async function loadObjects() { + async function loadObjects(continuationToken = null, isPageChange = false) { if (!currentBucket) return; showTableLoading('objects-table', 6); clearSelection(); + if (!isPageChange) { + currentPage = 1; + continuationTokens = [null]; + } updateBreadcrumb(); try { - const result = await api.listObjectsV2(currentBucket, currentPrefix); + const result = await api.listObjectsV2(currentBucket, currentPrefix, '/', pageSize, continuationToken); + isTruncated = result.isTruncated; + if (isTruncated && result.continuationToken) { + continuationTokens[currentPage] = result.continuationToken; + } folders = result.commonPrefixes || []; objects = result.contents || []; @@ -1594,11 +1645,29 @@ under the License. } } + // When paginating, restrict deleted-with-versions objects to the current page's + // key range, since listObjectVersions returns all versions (not page-bounded). + if (isTruncated || currentPage > 1) { + const allPageKeys = [ + ...folders.map(f => f.prefix), + ...objects.map(o => o.key) + ].sort(); + const pageFirstKey = allPageKeys[0]; + const pageLastKey = allPageKeys[allPageKeys.length - 1]; + deletedObjectsWithVersions = deletedObjectsWithVersions.filter(obj => { + if (currentPage > 1 && pageFirstKey && obj.key < pageFirstKey) return false; + if (isTruncated && pageLastKey && obj.key > pageLastKey) return false; + return true; + }); + } + renderObjects(); + return true; } catch (error) { console.error('Error loading objects:', error); showToast('Error loading objects: ' + error.message, 'error'); showEmptyState('objects-table', 6, 'Error loading objects'); + return false; } } @@ -1642,6 +1711,7 @@ under the License. // Empty folder or no results if (filteredFolders.length === 0 && filteredObjects.length === 0 && filteredDeletedObjects.length === 0) { + document.getElementById('objects-pagination').classList.add('hidden'); if (searchTerm) { tbody.innerHTML = ` @@ -1677,6 +1747,9 @@ under the License. return; } + // Count after filtering (objects.filter above may have removed the prefix key) + const totalVisible = filteredFolders.length + filteredObjects.length + filteredDeletedObjects.length; + // Render folders first filteredFolders.forEach(folder => { const name = getFolderName(folder.prefix); @@ -1839,6 +1912,96 @@ under the License. `; tbody.appendChild(row); }); + + // Show and update pagination bar + updatePagination(totalVisible); + } + + // ============================================ + // Pagination + // ============================================ + + function updatePagination(itemCount) { + const bar = document.getElementById('objects-pagination'); + bar.classList.remove('hidden'); + + const startItem = (currentPage - 1) * pageSize + 1; + const endItem = (currentPage - 1) * pageSize + itemCount; + const searchNote = searchTerm ? ' (filtered)' : ''; + document.getElementById('pagination-info').textContent = + itemCount > 0 ? `${startItem}–${endItem}${isTruncated ? '+' : ''} items${searchNote}` : ''; + document.getElementById('pagination-pages').textContent = + `Page ${currentPage}${isTruncated ? '' : ' (last)'}`; + + const firstBtn = document.getElementById('pagination-first'); + const prevBtn = document.getElementById('pagination-prev'); + const nextBtn = document.getElementById('pagination-next'); + + const isFirst = currentPage <= 1; + const isLast = !isTruncated || !!searchTerm; // disable forward nav while search is active + firstBtn.disabled = isFirst; + prevBtn.disabled = isFirst; + nextBtn.disabled = isLast; + + // Sync the select to the current pageSize + const select = document.getElementById('page-size-select'); + if (select) select.value = String(pageSize); + } + + async function changePage(page) { + if (page < 1) return false; + if (page === currentPage) return false; + if (page > currentPage && !isTruncated) return false; + const token = continuationTokens[page - 1]; + if (token === undefined) return false; // token not yet known; can't jump + + const previousPage = currentPage; + currentPage = page; + const loaded = await loadObjects(token, true); + if (!loaded) { + currentPage = previousPage; + renderObjects(); + } + + return loaded; + } + + async function changePageSize(size) { + if (size === pageSize) return false; + + const previousPageSize = pageSize; + const previousPage = currentPage; + const previousTokens = [...continuationTokens]; + + pageSize = size; + currentPage = 1; + continuationTokens = [null]; + const loaded = await loadObjects(); + if (!loaded) { + pageSize = previousPageSize; + currentPage = previousPage; + continuationTokens = previousTokens; + renderObjects(); + } + + return loaded; + } + + async function reloadFirstPageForSearch() { + if (isReloadingFirstPageForSearch) return; + + const previousTokens = [...continuationTokens]; + + isReloadingFirstPageForSearch = true; + continuationTokens = [null]; + try { + const loaded = await changePage(1); + if (!loaded) { + continuationTokens = previousTokens; + } + } finally { + isReloadingFirstPageForSearch = false; + } } @@ -1920,6 +2083,7 @@ under the License. function navigateToFolder(prefix) { currentPrefix = prefix; + currentPage = 1; loadObjects(); updateUrl(); } @@ -3053,6 +3217,12 @@ under the License. } else { clearBtn.classList.add('hidden'); } + + if (currentBucket && (currentPage > 1 || isReloadingFirstPageForSearch)) { + reloadFirstPageForSearch(); + return; + } + renderObjects(); }, 200)); } @@ -3068,7 +3238,15 @@ under the License. const clearBtn = document.getElementById('clear-search'); if (searchInput) searchInput.value = ''; if (clearBtn) clearBtn.classList.add('hidden'); - if (currentBucket) renderObjects(); + + if (!currentBucket) return; + + if (currentPage > 1 || isReloadingFirstPageForSearch) { + reloadFirstPageForSearch(); + return; + } + + renderObjects(); } // ============================================