feat(api): add server-side pagination and sorting to event logs API
Some checks failed
CI / Lint (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Test (1.22) (push) Has been cancelled
CI / Test (1.23) (push) Has been cancelled

- update event logs API to support pagination and sorting via headers
- modify event logs page to handle new API response structure
- implement debounced search functionality for improved UX
- adjust total count display to reflect actual number of logs
This commit is contained in:
2026-02-20 21:51:09 +02:00
parent b81febafc9
commit a4eb2a175c
9 changed files with 321 additions and 122 deletions

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"io/fs"
"net/http"
"strings"
"time"
"github.com/google/uuid"
@@ -307,13 +308,17 @@ func handleLogout(w http.ResponseWriter, r *http.Request, secProvider security.S
// QueryRequest represents a unified query request
type QueryRequest struct {
Action string `json:"action"` // "list", "get", "create", "update", "delete"
Table string `json:"table"` // Table name (e.g., "users", "hooks")
ID string `json:"id,omitempty"` // For get/update/delete
Data map[string]interface{} `json:"data,omitempty"` // For create/update
Filters map[string]interface{} `json:"filters,omitempty"` // For list
Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"`
Action string `json:"action"` // "list", "get", "create", "update", "delete"
Table string `json:"table"` // Table name (e.g., "users", "hooks")
ID string `json:"id,omitempty"` // For get/update/delete
Data map[string]interface{} `json:"data,omitempty"` // For create/update
Filters map[string]interface{} `json:"filters,omitempty"` // For list — exact match
Search string `json:"search,omitempty"` // For list — LIKE across SearchColumns
SearchColumns []string `json:"search_columns,omitempty"` // Columns to apply Search against
OrderBy string `json:"order_by,omitempty"` // Column to order by
OrderDir string `json:"order_dir,omitempty"` // "ASC" or "DESC"
Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"`
}
// handleQuery handles unified query requests
@@ -348,24 +353,45 @@ func handleQuery(w http.ResponseWriter, r *http.Request, db *bun.DB, secProvider
}
}
// applySearchAndFilters applies exact-match filters and LIKE search to a select query.
func applySearchAndFilters(query *bun.SelectQuery, req QueryRequest) *bun.SelectQuery {
for key, value := range req.Filters {
query = query.Where("? = ?", bun.Ident(key), value)
}
if req.Search != "" && len(req.SearchColumns) > 0 {
query = query.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
for i, col := range req.SearchColumns {
if i == 0 {
q = q.Where("? LIKE ?", bun.Ident(col), "%"+req.Search+"%")
} else {
q = q.WhereOr("? LIKE ?", bun.Ident(col), "%"+req.Search+"%")
}
}
return q
})
}
return query
}
// handleQueryList lists records from a table
func handleQueryList(w http.ResponseWriter, r *http.Request, db *bun.DB, req QueryRequest, userCtx *security.UserContext) {
// Get model registry to find the model
registry := getModelForTable(req.Table)
if registry == nil {
http.Error(w, "Table not found", http.StatusNotFound)
return
}
// Create slice to hold results
results := registry()
// Build query
query := db.NewSelect().Model(results)
query = applySearchAndFilters(query, req)
// Apply filters
for key, value := range req.Filters {
query = query.Where("? = ?", bun.Ident(key), value)
// Apply ordering
if req.OrderBy != "" {
dir := "ASC"
if strings.ToUpper(req.OrderDir) == "DESC" {
dir = "DESC"
}
query = query.OrderExpr("? "+dir, bun.Ident(req.OrderBy))
}
// Apply limit/offset
@@ -376,9 +402,8 @@ func handleQueryList(w http.ResponseWriter, r *http.Request, db *bun.DB, req Que
query = query.Offset(req.Offset)
}
// Execute query
if err := query.Scan(r.Context()); err != nil {
http.Error(w, fmt.Sprintf("Query failed: %v", err), http.StatusInternalServerError)
http.Error(w, "Query failed", http.StatusInternalServerError)
return
}

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/ui/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>web</title>
<script type="module" crossorigin src="/ui/assets/index-CExXKuWO.js"></script>
<script type="module" crossorigin src="/ui/assets/index-Cj4Q_Qgu.js"></script>
<link rel="stylesheet" crossorigin href="/ui/assets/index-Bfia8Lvm.css">
</head>
<body>