feat(admin): add dashboard overview charts and log pagination
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m40s

Enhance the admin panel by introducing visual overview charts for upload and storage trends, along with status bars for system metrics.

Additionally, implement pagination for the admin logs view, allowing users to navigate through log entries with configurable page sizes. Corresponding CSS styles have been added for the new charts, metrics grid, and pagination controls.
This commit is contained in:
2026-06-01 04:22:38 +03:00
parent cc91ce120d
commit ffa2d9636b
9 changed files with 618 additions and 111 deletions

View File

@@ -14,27 +14,40 @@ import (
"warpbox.dev/backend/libs/web"
)
const adminFilesPageSize = 25
const adminFilesDefaultPageSize = 50
var adminFilesPageSizes = []int{25, 50, 100, 200}
type adminFilesData struct {
Stats services.AdminStats
Section string
PageTitle string
Boxes []adminBoxView
Query string
Sort string
Dir string
Page int
TotalPages int
Total int
RangeFrom int
RangeTo int
Columns []adminFilesColumn
PageLinks []adminFilesPageLink
HasPrev bool
HasNext bool
PrevHref string
NextHref string
Stats services.AdminStats
Section string
PageTitle string
Boxes []adminBoxView
Query string
Sort string
Dir string
Page int
PerPage int
PerPageOptions []int
TotalPages int
Total int
RangeFrom int
RangeTo int
Columns []adminFilesColumn
PageLinks []adminFilesPageLink
HasPrev bool
HasNext bool
PrevHref string
NextHref string
}
// adminFilesQuery captures the listing state that every paginated link must
// preserve.
type adminFilesQuery struct {
Query string
Sort string
Dir string
Per int
}
type adminFilesColumn struct {
@@ -153,8 +166,11 @@ func (a *App) AdminFiles(w http.ResponseWriter, r *http.Request) {
}
sortAdminFileRows(rows, sortKey, dir)
perPage := normalizePageSize(r.URL.Query().Get("per"), adminFilesDefaultPageSize, adminFilesPageSizes)
state := adminFilesQuery{Query: query, Sort: sortKey, Dir: dir, Per: perPage}
total := len(rows)
totalPages := (total + adminFilesPageSize - 1) / adminFilesPageSize
totalPages := (total + perPage - 1) / perPage
if totalPages < 1 {
totalPages = 1
}
@@ -165,11 +181,11 @@ func (a *App) AdminFiles(w http.ResponseWriter, r *http.Request) {
if page > totalPages {
page = totalPages
}
start := (page - 1) * adminFilesPageSize
start := (page - 1) * perPage
if start > total {
start = total
}
end := start + adminFilesPageSize
end := start + perPage
if end > total {
end = total
}
@@ -200,24 +216,26 @@ func (a *App) AdminFiles(w http.ResponseWriter, r *http.Request) {
Description: "Manage Warpbox uploads.",
CurrentUser: a.currentPublicUser(r),
Data: adminFilesData{
Stats: stats,
Section: "files",
PageTitle: "Files",
Boxes: views,
Query: query,
Sort: sortKey,
Dir: dir,
Page: page,
TotalPages: totalPages,
Total: total,
RangeFrom: rangeFrom,
RangeTo: end,
Columns: adminFilesColumns(query, sortKey, dir),
PageLinks: adminFilesPageLinks(query, sortKey, dir, page, totalPages),
HasPrev: page > 1,
HasNext: page < totalPages,
PrevHref: adminFilesHref(query, sortKey, dir, page-1),
NextHref: adminFilesHref(query, sortKey, dir, page+1),
Stats: stats,
Section: "files",
PageTitle: "Files",
Boxes: views,
Query: query,
Sort: sortKey,
Dir: dir,
Page: page,
PerPage: perPage,
PerPageOptions: adminFilesPageSizes,
TotalPages: totalPages,
Total: total,
RangeFrom: rangeFrom,
RangeTo: end,
Columns: adminFilesColumns(state, sortKey, dir),
PageLinks: adminFilesPageLinks(state, page, totalPages),
HasPrev: page > 1,
HasNext: page < totalPages,
PrevHref: adminFilesHref(state, page-1),
NextHref: adminFilesHref(state, page+1),
},
})
}
@@ -274,7 +292,7 @@ func sortAdminFileRows(rows []adminFileRow, sortKey, dir string) {
})
}
func adminFilesColumns(query, sortKey, dir string) []adminFilesColumn {
func adminFilesColumns(state adminFilesQuery, sortKey, dir string) []adminFilesColumn {
defs := []struct{ Key, Label string }{
{"id", "Box"},
{"owner", "Owner"},
@@ -291,9 +309,12 @@ func adminFilesColumns(query, sortKey, dir string) []adminFilesColumn {
if sorted && dir == "asc" {
nextDir = "desc"
}
colState := state
colState.Sort = def.Key
colState.Dir = nextDir
columns = append(columns, adminFilesColumn{
Label: def.Label,
Href: adminFilesHref(query, def.Key, nextDir, 1),
Href: adminFilesHref(colState, 1),
Sorted: sorted,
Ascending: dir == "asc",
})
@@ -301,7 +322,7 @@ func adminFilesColumns(query, sortKey, dir string) []adminFilesColumn {
return columns
}
func adminFilesPageLinks(query, sortKey, dir string, page, totalPages int) []adminFilesPageLink {
func adminFilesPageLinks(state adminFilesQuery, page, totalPages int) []adminFilesPageLink {
links := make([]adminFilesPageLink, 0, 5)
const window = 2
for p := page - window; p <= page+window; p++ {
@@ -310,23 +331,26 @@ func adminFilesPageLinks(query, sortKey, dir string, page, totalPages int) []adm
}
links = append(links, adminFilesPageLink{
Page: p,
Href: adminFilesHref(query, sortKey, dir, p),
Href: adminFilesHref(state, p),
Active: p == page,
})
}
return links
}
func adminFilesHref(query, sortKey, dir string, page int) string {
func adminFilesHref(state adminFilesQuery, page int) string {
values := url.Values{}
if query != "" {
values.Set("q", query)
if state.Query != "" {
values.Set("q", state.Query)
}
if sortKey != "" && sortKey != "created" {
values.Set("sort", sortKey)
if state.Sort != "" && state.Sort != "created" {
values.Set("sort", state.Sort)
}
if dir != "" && dir != "desc" {
values.Set("dir", dir)
if state.Dir != "" && state.Dir != "desc" {
values.Set("dir", state.Dir)
}
if state.Per > 0 && state.Per != adminFilesDefaultPageSize {
values.Set("per", strconv.Itoa(state.Per))
}
if page > 1 {
values.Set("page", strconv.Itoa(page))
@@ -337,6 +361,21 @@ func adminFilesHref(query, sortKey, dir string, page int) string {
return "/admin/files?" + values.Encode()
}
// normalizePageSize parses a requested page size, falling back to def when the
// value is missing or not one of the allowed sizes.
func normalizePageSize(raw string, def int, allowed []int) int {
parsed, err := strconv.Atoi(strings.TrimSpace(raw))
if err != nil {
return def
}
for _, size := range allowed {
if size == parsed {
return parsed
}
}
return def
}
func (a *App) AdminEditBox(w http.ResponseWriter, r *http.Request) {
if !a.requireAdmin(w, r) {
return