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();
}
// ============================================