Skip to content

Commit 8257ad9

Browse files
authored
Merge pull request #734 from zhuoyuan-liu/pagination
Enhance pagination and search functionality for nodes in JSON endpoints
2 parents 63b73d0 + c75f392 commit 8257ad9

File tree

3 files changed

+133
-25
lines changed

3 files changed

+133
-25
lines changed

cmd/admin/handlers/json-nodes.go

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -167,37 +167,44 @@ func (h *HandlersAdmin) JSONEnvironmentPagingHandler(w http.ResponseWriter, r *h
167167
start, _ := strconv.Atoi(r.URL.Query().Get("start"))
168168
length, _ := strconv.Atoi(r.URL.Query().Get("length"))
169169
searchValue := r.URL.Query().Get("search")
170-
// Get total nodes count
171-
totalCount, err := h.Nodes.CountAllByEnv(env.ID)
170+
171+
// DB-level counts
172+
totalCount, err := h.Nodes.CountByEnvTarget(env.Name, target, h.Settings.InactiveHours(settings.NoEnvironmentID))
172173
if err != nil {
173-
log.Err(err).Msg("error getting total nodes count")
174+
log.Err(err).Msg("error counting total nodes")
174175
return
175176
}
176-
var nodes []nodes.OsqueryNode
177-
// If there is a search value, run the search
177+
var filteredCount int64
178+
var nodesSlice []nodes.OsqueryNode
179+
hours := h.Settings.InactiveHours(settings.NoEnvironmentID)
180+
// Ordering (DataTables sends order[0][column], order[0][dir])
181+
orderColIdxStr := r.URL.Query().Get("order[0][column]")
182+
orderDir := r.URL.Query().Get("order[0][dir]")
183+
colName := mapDTColumnToDB(orderColIdxStr)
184+
desc := (orderDir == "desc")
178185
if searchValue != "" {
179-
nodes, err = h.Nodes.SearchByEnv(env.Name, searchValue, target, h.Settings.InactiveHours(settings.NoEnvironmentID))
186+
// Count filtered first
187+
filteredCount, err = h.Nodes.CountSearchByEnv(env.Name, searchValue, target, hours)
180188
if err != nil {
181-
log.Err(err).Msg("error searching nodes")
189+
log.Err(err).Msg("error counting search nodes")
190+
return
191+
}
192+
nodesSlice, err = h.Nodes.SearchByEnvPage(env.Name, searchValue, target, hours, start, length, colName, desc)
193+
if err != nil {
194+
log.Err(err).Msg("error searching nodes page")
182195
return
183196
}
184197
} else {
185-
nodes, err = h.Nodes.GetByEnvLimit(env.Name, target, h.Settings.InactiveHours(settings.NoEnvironmentID))
198+
filteredCount = totalCount
199+
nodesSlice, err = h.Nodes.GetByEnvPage(env.Name, target, hours, start, length, colName, desc)
186200
if err != nil {
187-
log.Err(err).Msg("error getting nodes")
201+
log.Err(err).Msg("error getting nodes page")
188202
return
189203
}
190204
}
191-
// Pagination, it can be done more efficiently in the DB, but this is ok for now
192-
end := min(start+length, len(nodes))
193-
if start > end {
194-
start = end
195-
}
196-
filteredCount := len(nodes)
197-
nodes = nodes[start:end]
198205
// Prepare data to be returned
199206
nJSON := []NodeJSON{}
200-
for _, n := range nodes {
207+
for _, n := range nodesSlice {
201208
nj := NodeJSON{
202209
UUID: n.UUID,
203210
Username: n.Username,
@@ -220,9 +227,38 @@ func (h *HandlersAdmin) JSONEnvironmentPagingHandler(w http.ResponseWriter, r *h
220227
returned := PaginatedNodes{
221228
Draw: draw,
222229
Total: int(totalCount),
223-
Filtered: filteredCount,
230+
Filtered: int(filteredCount),
224231
Data: nJSON,
225232
}
226233
// Serve JSON
227234
utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, returned)
228235
}
236+
237+
// mapDTColumnToDB maps DataTables column index (as string) to actual DB column name.
238+
// DataTables columns order in the UI:
239+
// 0 checkbox,1 uuid,2 username(last user),3 localname,4 ip,5 platform,6 version(platform_version),7 osquery,8 lastseen,9 firstseen
240+
// We only allow ordering on a safe subset of real DB columns.
241+
func mapDTColumnToDB(idx string) string {
242+
switch idx {
243+
case "1":
244+
return "uuid"
245+
case "2":
246+
return "username"
247+
case "3":
248+
return "localname"
249+
case "4":
250+
return "ip_address"
251+
case "5":
252+
return "platform"
253+
case "6":
254+
return "platform_version"
255+
case "7":
256+
return "osquery_version"
257+
case "8":
258+
return "last_seen"
259+
case "9":
260+
return "created_at" // first seen
261+
default:
262+
return "" // fallback to default ordering in caller
263+
}
264+
}

cmd/admin/templates/table.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,17 @@ <h4 class="modal-title">Tag nodes</h4>
127127
{{ if .OptimizeUI }}
128128
url: "/paginated-json/{{ .Selector }}/{{ .SelectorName }}/{{ .Target }}",
129129
data: function(d) {
130+
// Explicitly forward ordering parameters expected by server handler
131+
// while preserving the simplified search parameter name already used server-side.
132+
var orderCol = (d.order && d.order.length) ? d.order[0].column : 8; // default Last Seen
133+
var orderDir = (d.order && d.order.length) ? d.order[0].dir : 'desc';
130134
return {
131135
draw: d.draw,
132136
start: d.start,
133137
length: d.length,
134-
search: d.search.value
138+
search: d.search.value,
139+
'order[0][column]': orderCol,
140+
'order[0][dir]': orderDir
135141
};
136142
},
137143
{{ else }}

pkg/nodes/nodes.go

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -198,20 +198,46 @@ func (n *NodeManager) GetByEnv(env, target string, hours int64) ([]OsqueryNode,
198198
return nodes, nil
199199
}
200200

201-
// GetByEnvLimit to retrieve target nodes by environment with limit and offset
202-
func (n *NodeManager) GetByEnvLimit(env, target string, hours int64) ([]OsqueryNode, error) {
201+
// GetByEnvPage retrieves a page of nodes by environment applying target filters using LIMIT/OFFSET
202+
func (n *NodeManager) GetByEnvPage(env, target string, hours int64, offset, limit int, orderBy string, desc bool) ([]OsqueryNode, error) {
203203
var nodes []OsqueryNode
204-
// Build query with base condition
204+
if limit <= 0 { // safety default
205+
limit = 25
206+
}
207+
if limit > 500 { // cap to avoid abuse
208+
limit = 500
209+
}
210+
if offset < 0 {
211+
offset = 0
212+
}
205213
query := n.DB.Where("environment = ?", env)
206-
// Apply active/inactive filtering
207214
query = ApplyNodeTarget(query, target, hours)
208-
// Execute query
209-
if err := query.Find(&nodes).Error; err != nil {
215+
// Default ordering only if client did not request a specific column
216+
orderExpr := "last_seen DESC"
217+
if orderBy != "" {
218+
direction := "ASC"
219+
if desc {
220+
direction = "DESC"
221+
}
222+
orderExpr = orderBy + " " + direction
223+
}
224+
if err := query.Order(orderExpr).Offset(offset).Limit(limit).Find(&nodes).Error; err != nil {
210225
return nodes, err
211226
}
212227
return nodes, nil
213228
}
214229

230+
// CountByEnvTarget counts nodes for an environment after applying target (active/inactive/all)
231+
func (n *NodeManager) CountByEnvTarget(env string, target string, hours int64) (int64, error) {
232+
var count int64
233+
query := n.DB.Model(&OsqueryNode{}).Where("environment = ?", env)
234+
query = ApplyNodeTarget(query, target, hours)
235+
if err := query.Count(&count).Error; err != nil {
236+
return 0, err
237+
}
238+
return count, nil
239+
}
240+
215241
// SearchByEnv to search nodes by environment and search term
216242
func (n *NodeManager) SearchByEnv(env, term, target string, hours int64) ([]OsqueryNode, error) {
217243
var nodes []OsqueryNode
@@ -227,6 +253,46 @@ func (n *NodeManager) SearchByEnv(env, term, target string, hours int64) ([]Osqu
227253
return nodes, nil
228254
}
229255

256+
// SearchByEnvPage performs a paginated search
257+
func (n *NodeManager) SearchByEnvPage(env, term, target string, hours int64, offset, limit int, orderBy string, desc bool) ([]OsqueryNode, error) {
258+
if limit <= 0 {
259+
limit = 25
260+
} else if limit > 500 {
261+
limit = 500
262+
}
263+
if offset < 0 {
264+
offset = 0
265+
}
266+
var nodes []OsqueryNode
267+
likeTerm := "%" + term + "%"
268+
query := n.DB.Where("environment = ? AND (uuid LIKE ? OR hostname LIKE ? OR localname LIKE ? OR ip_address LIKE ? OR username LIKE ? OR osquery_user LIKE ? OR platform LIKE ? OR osquery_version LIKE ?)", env, likeTerm, likeTerm, likeTerm, likeTerm, likeTerm, likeTerm, likeTerm, likeTerm)
269+
query = ApplyNodeTarget(query, target, hours)
270+
orderExpr := "last_seen DESC"
271+
if orderBy != "" {
272+
direction := "ASC"
273+
if desc {
274+
direction = "DESC"
275+
}
276+
orderExpr = orderBy + " " + direction
277+
}
278+
if err := query.Order(orderExpr).Offset(offset).Limit(limit).Find(&nodes).Error; err != nil {
279+
return nodes, err
280+
}
281+
return nodes, nil
282+
}
283+
284+
// CountSearchByEnv counts matching nodes for a search term with target filters
285+
func (n *NodeManager) CountSearchByEnv(env, term, target string, hours int64) (int64, error) {
286+
likeTerm := "%" + term + "%"
287+
query := n.DB.Model(&OsqueryNode{}).Where("environment = ? AND (uuid LIKE ? OR hostname LIKE ? OR localname LIKE ? OR ip_address LIKE ? OR username LIKE ? OR osquery_user LIKE ? OR platform LIKE ? OR osquery_version LIKE ?)", env, likeTerm, likeTerm, likeTerm, likeTerm, likeTerm, likeTerm, likeTerm, likeTerm)
288+
query = ApplyNodeTarget(query, target, hours)
289+
var count int64
290+
if err := query.Count(&count).Error; err != nil {
291+
return 0, err
292+
}
293+
return count, nil
294+
}
295+
230296
// GetByPlatform to retrieve target nodes by platform
231297
func (n *NodeManager) GetByPlatform(envID uint, platform, target string, hours int64) ([]OsqueryNode, error) {
232298
var nodes []OsqueryNode

0 commit comments

Comments
 (0)