admin: add cursor-based pagination to file browser (#7891)

* adjust menu items

* admin: add cursor-based pagination to file browser

- Implement cursor-based pagination using lastFileName parameter
- Add customizable page size selector (20/50/100/200 entries)
- Add compact pagination controls in header and footer
- Remove summary cards for cleaner UI
- Make directory names clickable to return to first page
- Support forward-only navigation (Next button)
- Preserve cursor position when changing page size
- Remove sorting to align with filer's storage order approach

* Update file_browser_templ.go

* admin: remove directory icons from breadcrumbs

* Update file_browser_templ.go

* admin: address PR comments

- Fix fragile EOF check: use io.EOF instead of string comparison
- Cap page size at 200 to prevent potential DoS
- Remove unused helper functions from template
- Use safer templ script for page size selector to prevent XSS

* admin: cleanup redundant first button

* Update file_browser_templ.go

* admin: remove entry counting logic

* admin: remove unused variables in file browser data

* admin: remove unused logic for FirstFileName and HasPrevPage

* admin: remove unused TotalEntries and TotalSize fields

* Update file_browser_data.go
This commit is contained in:
Chris Lu
2025-12-27 02:12:57 -08:00
committed by GitHub
parent 8d6bcddf60
commit 6de6061ce9
6 changed files with 542 additions and 420 deletions

View File

@@ -7,6 +7,10 @@ import (
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
)
script changePageSize(path string, lastFileName string) {
window.location.href = '/files?path=' + encodeURIComponent(path) + '&lastFileName=' + encodeURIComponent(lastFileName) + '&limit=' + this.value
}
templ FileBrowser(data dash.FileBrowserData) {
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">
@@ -45,15 +49,15 @@ templ FileBrowser(data dash.FileBrowserData) {
for i, crumb := range data.Breadcrumbs {
if i == len(data.Breadcrumbs)-1 {
<li class="breadcrumb-item active" aria-current="page">
<i class="fas fa-folder me-1"></i>{ crumb.Name }
<a href={ templ.SafeURL(fmt.Sprintf("/files?path=%s", crumb.Path)) } class="text-decoration-none">
{ crumb.Name }
</a>
</li>
} else {
<li class="breadcrumb-item">
<a href={ templ.SafeURL(fmt.Sprintf("/files?path=%s", crumb.Path)) } class="text-decoration-none">
if crumb.Name == "Root" {
<i class="fas fa-home me-1"></i>
} else {
<i class="fas fa-folder me-1"></i>
}
{ crumb.Name }
</a>
@@ -63,110 +67,51 @@ templ FileBrowser(data dash.FileBrowserData) {
</ol>
</nav>
<!-- Summary Cards -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
Total Entries
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{ fmt.Sprintf("%d", data.TotalEntries) }
</div>
</div>
<div class="col-auto">
<i class="fas fa-list fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
Directories
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{ fmt.Sprintf("%d", countDirectories(data.Entries)) }
</div>
</div>
<div class="col-auto">
<i class="fas fa-folder fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
Files
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{ fmt.Sprintf("%d", countFiles(data.Entries)) }
</div>
</div>
<div class="col-auto">
<i class="fas fa-file fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-warning shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
Total Size
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{ formatBytes(data.TotalSize) }
</div>
</div>
<div class="col-auto">
<i class="fas fa-hdd fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- File Listing -->
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<div class="card-header py-3 d-flex justify-content-between align-items-center flex-wrap">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-folder-open me-2"></i>
if data.CurrentPath == "/" {
Root Directory
<a href="/files?path=/" class="text-decoration-none text-primary">Root Directory</a>
} else if data.CurrentPath == "/buckets" {
Object Store Buckets Directory
<a href="/files?path=/buckets" class="text-decoration-none text-primary">Object Store Buckets Directory</a>
<a href="/object-store/buckets" class="btn btn-sm btn-outline-primary ms-2">
<i class="fas fa-cube me-1"></i>Manage Buckets
</a>
} else {
{ filepath.Base(data.CurrentPath) }
<a href={ templ.SafeURL(fmt.Sprintf("/files?path=%s", data.CurrentPath)) } class="text-decoration-none text-primary">{ filepath.Base(data.CurrentPath) }</a>
}
</h6>
if data.ParentPath != data.CurrentPath {
<a href={ templ.SafeURL(fmt.Sprintf("/files?path=%s", data.ParentPath)) } class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-up me-1"></i>Up
</a>
}
<!-- Compact pagination controls -->
<div class="d-flex align-items-center gap-2 ms-auto">
<div class="d-flex align-items-center">
<label class="me-1 mb-0 small text-muted">Show:</label>
<select class="form-select form-select-sm" style="width: 70px; font-size: 0.875rem;" onchange={ changePageSize(data.CurrentPath, data.CurrentLastFileName) }>
<option value="20" selected?={ data.PageSize == 20 }>20</option>
<option value="50" selected?={ data.PageSize == 50 }>50</option>
<option value="100" selected?={ data.PageSize == 100 }>100</option>
<option value="200" selected?={ data.PageSize == 200 }>200</option>
</select>
</div>
<div class="btn-group btn-group-sm" role="group">
if data.HasNextPage {
<a href={ templ.SafeURL(fmt.Sprintf("/files?path=%s&lastFileName=%s&limit=%d", data.CurrentPath, data.LastFileName, data.PageSize)) } class="btn btn-outline-primary" title="Next page">
Next <i class="fas fa-angle-right"></i>
</a>
} else {
<button class="btn btn-outline-secondary" disabled title="Next page">
Next <i class="fas fa-angle-right"></i>
</button>
}
if data.ParentPath != data.CurrentPath {
<a href={ templ.SafeURL(fmt.Sprintf("/files?path=%s", data.ParentPath)) } class="btn btn-outline-secondary" title="Go up one directory">
<i class="fas fa-arrow-up"></i> Up
</a>
}
</div>
</div>
</div>
<div class="card-body">
if len(data.Entries) > 0 {
@@ -264,6 +209,36 @@ templ FileBrowser(data dash.FileBrowserData) {
</div>
<!-- Last Updated -->
<!-- Pagination Controls (Bottom) -->
<div class="row mb-3">
<div class="col-md-6">
<div class="d-flex align-items-center">
<label class="me-2 mb-0">Show:</label>
<select class="form-select form-select-sm" style="width: auto;" onchange={ changePageSize(data.CurrentPath, data.CurrentLastFileName) }>
<option value="20" selected?={ data.PageSize == 20 }>20</option>
<option value="50" selected?={ data.PageSize == 50 }>50</option>
<option value="100" selected?={ data.PageSize == 100 }>100</option>
<option value="200" selected?={ data.PageSize == 200 }>200</option>
</select>
<span class="ms-2 text-muted">entries per page</span>
</div>
</div>
<div class="col-md-6 text-end">
<div class="btn-group btn-group-sm" role="group">
if data.HasNextPage {
<a href={ templ.SafeURL(fmt.Sprintf("/files?path=%s&lastFileName=%s&limit=%d", data.CurrentPath, data.LastFileName, data.PageSize)) } class="btn btn-outline-primary">
Next <i class="fas fa-angle-right ms-1"></i>
</a>
} else {
<button class="btn btn-outline-secondary" disabled>
Next <i class="fas fa-angle-right ms-1"></i>
</button>
}
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<small class="text-muted">
@@ -732,25 +707,7 @@ templ FileBrowser(data dash.FileBrowserData) {
</script>
}
func countDirectories(entries []dash.FileEntry) int {
count := 0
for _, entry := range entries {
if entry.IsDirectory {
count++
}
}
return count
}
func countFiles(entries []dash.FileEntry) int {
count := 0
for _, entry := range entries {
if !entry.IsDirectory {
count++
}
}
return count
}
func getFileIcon(mime string) string {
switch {