summaryrefslogtreecommitdiff
path: root/docs/tui-dashboard-table-sorting-plan.md
blob: 0d4586e5cf3ea11635f18e587b3691f0382c6eaf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# TUI Dashboard Table Sorting Plan

## Overview

Add column-driven sorting to the dashboard table views for:

- `3:Syscalls`
- `4:Files`
- `5:Processes`

This is a **table-view-only** feature. Bubble, treemap, and icicle modes keep
their existing ordering rules.

The task wording says "sort by any row", but the current dashboard already
tracks both a selected row and a selected column. This plan therefore treats
`s` as **sort by the currently selected column/cell**.

Pressing `s`:

1. on a new selected column enables that column's sort order
2. again on the same selected column clears the custom sort and restores the
   tab's current default ordering

## Current Behavior

The dashboard already has the key pieces needed for this feature:

- `internal/tui/dashboard/model.go`
  - stores row selection and selected column for Syscalls, Files, and Processes
  - routes table navigation with `left/right` and `h/l`
- `internal/tui/dashboard/syscalls.go`
  - renders the syscall table from `snap.Syscalls()`
- `internal/tui/dashboard/files.go`
  - renders both the file table and the grouped-directory table
- `internal/tui/dashboard/processes.go`
  - renders the process table

The current default ordering comes from the snapshot producers:

- Syscalls: `Count desc`, then `Name asc`
- Files: `Accesses desc`, then `Path asc`
- Grouped directories: `Accesses desc`, then `Directory asc`
- Processes: `Syscalls desc`, then `Bytes desc`, then `PID asc`

That ordering should remain the baseline whenever no custom sort is active.

## Design Goals

- `s` sorts by the selected column in table mode.
- `s` on the same selected column toggles back to the default ranking.
- `Enter` continues to act on the row currently visible on screen after sorting.
- Sorting stays in the dashboard layer; `statsengine` snapshot semantics do not
  change.
- Selection remains anchored to the same logical entity when sorting changes.
- Width changes do not corrupt sort state for the Syscalls tab.

## UX Rules

- `s` is active only for sortable dashboard tables:
  - Syscalls table mode
  - Files table mode
  - Files directory-grouped table mode
  - Processes table mode
- `s` does nothing in:
  - Overview
  - Latency+Gaps
  - Stream
  - Flame
  - bubble/treemap/icicle modes
- Table footer hints should add `s:sort`.
- The footer should also show the active sort, for example:
  - `sort: default`
  - `sort: p95 desc`
  - `sort: Path asc`
- Expanded help should mention `s` so the feature is discoverable.

## State Model

Add dashboard-local sort state per table shape.

Example shape:

```go
type tableSortState[K comparable] struct {
    active bool
    key    K
}
```

Recommended fields on `dashboard.Model`:

- `syscallsSort`
- `filesSort`
- `filesDirSort`
- `processesSort`

`Files` needs **two** sort states because the tab has two different table
schemas:

- file rows
- grouped directory rows

Those states should persist independently when `d` toggles between files and
directories.

## Logical Sort Keys

Do **not** store the raw selected column index as the sort identifier.

The Syscalls table changes shape by width:

- narrow layout: `Syscall Count Rate/s Avg p95 p99 Bytes Errors`
- wide layout: `Syscall Count Rate/s Avg Min Max p50 p95 p99 Bytes Errors`

If sort state stored only a column index, resizing from narrow to wide would
turn "sort by p95" into "sort by Min". The sort state must therefore use a
stable logical key enum, and map the current visible column index to that enum
at keypress time.

Recommended enums:

- `syscallSortKey`
- `fileSortKey`
- `fileDirSortKey`
- `processSortKey`

## Column Ordering Rules

Use a fixed natural direction per logical column. This avoids inventing a
three-state cycle and matches the task requirement of "sort" plus "toggle back".

### Syscalls

- `Syscall`: `Name asc`
- `Count`: `Count desc`
- `Rate/s`: `RatePerSec desc`
- `Avg`: `LatencyMeanNs desc`
- `Min`: `LatencyMinNs desc`
- `Max`: `LatencyMaxNs desc`
- `p50`: `LatencyP50Ns desc`
- `p95`: `LatencyP95Ns desc`
- `p99`: `LatencyP99Ns desc`
- `Bytes`: `Bytes desc`
- `Errors`: `Errors desc`

### Files

- `Accesses`: `Accesses desc`
- `Read`: `BytesRead desc`
- `Write`: `BytesWritten desc`
- `Avg Latency`: `AvgLatencyNs desc`
- `Max Latency`: `MaxLatencyNs desc`
- `Path`: `Path asc`

### Grouped Directories

- `Accesses`: `Accesses desc`
- `Read`: `BytesRead desc`
- `Write`: `BytesWritten desc`
- `Avg Latency`: `AvgLatencyNs desc`
- `Max Latency`: `MaxLatencyNs desc`
- `Files`: `FileCount desc`
- `Directory`: `Dir asc`

### Processes

- `PID`: `PID asc`
- `Comm`: `Comm asc`
- `Syscalls`: `Syscalls desc`
- `Rate/s`: `RatePerSec desc`
- `Total Bytes`: `Bytes desc`
- `Avg Latency`: `AvgLatencyNs desc`

## Comparator Rules

For deterministic output, custom comparators should fall back to the existing
default ranking for that row type.

Examples:

- `p95 desc`, then syscall default order
- `Path asc`, then file default order
- `Comm asc`, then process default order

This keeps ties stable and makes the "toggle back to default" behavior
predictable.

## Selection Anchoring

Changing sort order must not leave the cursor on the same numeric row index if
that index now points to a different entity.

Before toggling sort:

1. capture the currently selected logical entity key
2. recompute the sorted rows
3. restore the selected row to the same entity in the new order
4. if the entity no longer exists, clamp as today

Recommended identity keys:

- Syscalls: `Name`
- Files: `Path`
- Grouped directories: `Dir`
- Processes: `PID`

This same anchor logic should run on refresh ticks while custom sorting is
active so the selected item does not drift unpredictably as live stats change.

## Implementation Shape

Keep the sorting logic in `internal/tui/dashboard`, not in `internal/statsengine`.

Reason:

- snapshot order is part of the existing aggregate ranking behavior
- only the table presentation needs alternate ordering
- bubble/treemap/icicle already have their own ordering rules

Recommended implementation split:

- `internal/tui/common/keys.go`
  - add a `Sort` binding for `s`
  - include it in dashboard help output
- `internal/tui/dashboard/model.go`
  - add per-table sort state
  - handle `s`
  - ignore `s` outside sortable table modes
  - preserve selection anchors when sort changes
  - make `selectedSyscallFilter`, `selectedFileFilter`, and
    `selectedProcessSnapshot` read from the same sorted rows used by rendering
- `internal/tui/dashboard/syscalls.go`
  - add syscall sort key mapping from visible column index
  - add sorted syscall row helper
  - expose active sort label for footer hints
- `internal/tui/dashboard/files.go`
  - add file and directory sort key helpers
  - keep file and grouped-directory comparators separate
- `internal/tui/dashboard/processes.go`
  - add process sort key helpers and sorted row helper
- `internal/tui/dashboard/table.go`
  - extend footer hints/status rendering as needed for the active sort label

## Rendering/Data Consistency

The most important implementation rule is:

**the rendered rows and the row-selection actions must use the exact same sorted
slice**

Without this, the UI can show one row while `Enter` filters a different row.

The safest approach is to centralize each table's sorted typed rows in helper
functions and use those helpers in both:

- render paths
- selected-row action paths

## Files Tab Details

The Files tab needs one extra rule beyond Syscalls and Processes:

- in plain file mode, sorting operates on `[]statsengine.FileSnapshot`
- in directory-grouped mode, sorting operates on `[]DirSnapshot`

The two modes should not share a single sort key because their columns differ.
Switching with `d` should preserve:

- last file-table custom sort
- last directory-table custom sort

## Interaction With Existing Features

- `Enter`
  - still filters the currently selected visible row
- `d`
  - only changes Files table shape; custom sort state persists per mode
- `v`
  - custom sort state persists, but only applies when returning to table mode
- `b`
  - unaffected; bubble/treemap ordering remains metric-driven
- terminal resize
  - sort state persists because it stores logical keys, not raw indices
- trace restart / filter apply
  - sort state should remain as view state

## Testing Plan

Add focused tests in `internal/tui/dashboard` and `internal/tui/common`.

### Model behavior

- `s` on Syscalls enables a column sort.
- `s` on the same Syscalls column restores default sorting.
- `s` on Processes does nothing in non-table modes.
- `s` on Files uses file-mode sort state when `filesDirGrouped == false`.
- `s` on Files uses directory-mode sort state when `filesDirGrouped == true`.
- changing sort preserves the selected entity instead of only the row index.

### Width-sensitive syscall behavior

- sorting by `p95` in narrow mode survives a resize into wide mode
- sorting by `Syscall` or `Count` maps correctly in both layouts

### Selection action consistency

- `selectedSyscallFilter()` uses sorted syscall rows
- `selectedFileFilter()` uses sorted file or directory rows
- `selectedProcessSnapshot()` uses sorted process rows in table mode

### Help/footer rendering

- expanded help includes `s`
- table footer includes `s:sort`
- active sort label is visible in the table footer

### Negative cases

- `s` does nothing on Overview / Stream / Flame / Latency+Gaps
- `s` does nothing for bubble / treemap / icicle views

## Recommended Delivery Order

1. add key binding and sort state plumbing in `dashboard.Model`
2. implement sorted typed-row helpers per tab
3. switch render paths and selected-row actions to the shared helpers
4. add footer/help output
5. add regression tests for sort toggling, width changes, and selected-row
   action consistency

## Non-Goals

- no change to snapshot generation order in `statsengine`
- no sortable Overview or Latency+Gaps tables
- no ascending/descending toggle cycle beyond "custom sort" vs "default"
- no behavior change for bubble/treemap/icicle ordering