Sort all columns (#3650)

* feat: enhance table column selection and sorting functionality

* fix: update key binding for sorting selected column to Ctrl+O

* remove unused setSelectedColIdx method and sortIndicator function

* test: update expected hints count in various view tests

* test: update expected hints count in TestServiceNew

* fix: update keyboard handling for column selection to use Shift key

* refactor: streamline column selection logic and initialize selected column based on current sort

* fix: change key binding for sorting selected column from Ctrl+O to Shift+S

* test: update expected hints count in help, namespace, pod, and PVC tests
mine
Ümüt Özalp 2025-11-16 16:49:29 +01:00 committed by GitHub
parent 2bf2f481ef
commit 6bc67e97f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 216 additions and 42 deletions

View File

@ -39,6 +39,7 @@ type Table struct {
*SelectTable
gvr *client.GVR
sortCol model1.SortColumn
selectedColIdx int
manualSort bool
Path string
Extras string
@ -133,6 +134,137 @@ func (t *Table) getMSort() bool {
return t.manualSort
}
func (t *Table) getSelectedColIdx() int {
t.mx.RLock()
defer t.mx.RUnlock()
return t.selectedColIdx
}
// initSelectedColumn initializes the selected column index based on current sort column.
func (t *Table) initSelectedColumn() {
data := t.GetFilteredData()
if data == nil || data.HeaderCount() == 0 {
return
}
sc := t.getSortCol()
if sc.Name == "" {
t.mx.Lock()
t.selectedColIdx = 0
t.mx.Unlock()
return
}
// Find the visual column index for the current sort column
header := data.Header()
visibleCol := 0
for _, h := range header {
if t.shouldExcludeColumn(h) {
continue
}
if h.Name == sc.Name {
t.mx.Lock()
t.selectedColIdx = visibleCol
t.mx.Unlock()
return
}
visibleCol++
}
// If sort column not found in visible columns, default to 0
t.mx.Lock()
t.selectedColIdx = 0
t.mx.Unlock()
}
// moveSelectedColumn moves the column selection by delta (-1 for left, +1 for right).
func (t *Table) moveSelectedColumn(delta int) {
data := t.GetFilteredData()
if data == nil || data.HeaderCount() == 0 {
return
}
// Count visible columns
visibleCount := 0
for _, h := range data.Header() {
if !t.shouldExcludeColumn(h) {
visibleCount++
}
}
if visibleCount == 0 {
return
}
t.mx.Lock()
t.selectedColIdx += delta
// Wrap around
if t.selectedColIdx >= visibleCount {
t.selectedColIdx = 0
} else if t.selectedColIdx < 0 {
t.selectedColIdx = visibleCount - 1
}
t.mx.Unlock()
t.Refresh()
}
// SelectNextColumn moves the column selection to the right.
func (t *Table) SelectNextColumn() {
t.moveSelectedColumn(1)
}
// SelectPrevColumn moves the column selection to the left.
func (t *Table) SelectPrevColumn() {
t.moveSelectedColumn(-1)
}
// SortSelectedColumn sorts by the currently selected column.
func (t *Table) SortSelectedColumn() {
data := t.GetFilteredData()
if data == nil || data.HeaderCount() == 0 {
return
}
idx := t.getSelectedColIdx()
if idx < 0 {
return
}
// Map visual column index to actual header column name
// (accounting for hidden columns)
header := data.Header()
visibleCol := 0
var colName string
for _, h := range header {
if t.shouldExcludeColumn(h) {
continue
}
if visibleCol == idx {
colName = h.Name
break
}
visibleCol++
}
if colName == "" {
return
}
sc := t.getSortCol()
// Toggle direction if same column, otherwise default to ascending
asc := true
if sc.Name == colName {
asc = !sc.ASC
}
t.SetSortCol(colName, asc)
t.setMSort(true)
t.Refresh()
}
// SetViewSetting sets custom view config is present.
func (t *Table) SetViewSetting(vs *config.ViewSetting) bool {
t.mx.Lock()
@ -315,8 +447,17 @@ func (t *Table) doUpdate(data *model1.TableData) *model1.TableData {
} else {
t.actions.Delete(KeyShiftP)
}
oldSortCol := t.getSortCol()
t.setSortCol(data.ComputeSortCol(t.GetViewSetting(), t.getSortCol(), t.getMSort()))
// Initialize selected column index to match the current sort column
// This ensures the highlight starts at the sorted column
newSortCol := t.getSortCol()
if oldSortCol.Name != newSortCol.Name {
t.initSelectedColumn()
}
return data
}
@ -429,6 +570,10 @@ func (t *Table) SortColCmd(name string, asc bool) func(evt *tcell.EventKey) *tce
sc.Name = name
t.setSortCol(sc)
t.setMSort(true)
// Sync selected column index with the new sort column
t.initSelectedColumn()
t.Refresh()
return nil
}
@ -486,8 +631,9 @@ func (t *Table) NameColIndex() int {
func (t *Table) AddHeaderCell(col int, h model1.HeaderColumn) {
sc := t.getSortCol()
sortCol := h.Name == sc.Name
selectedCol := col == t.getSelectedColIdx()
styles := t.styles.Table()
c := tview.NewTableCell(sortIndicator(sortCol, sc.ASC, &styles, h.Name))
c := tview.NewTableCell(columnIndicator(sortCol, selectedCol, sc.ASC, &styles, h.Name))
c.SetExpansion(1)
c.SetSelectable(false)
c.SetAlign(h.Align)

View File

@ -84,16 +84,25 @@ func SkinTitle(fmat string, style *config.Frame) string {
return fmat
}
func sortIndicator(sort, asc bool, style *config.Table, name string) string {
if !sort {
return name
func columnIndicator(sort, selected, asc bool, style *config.Table, name string) string {
// Build the column name with selection indicator
displayName := name
if selected {
// Make selected column bold
displayName = fmt.Sprintf("[::b]%s[::-]", name)
}
// Add sort indicator if this column is sorted
suffix := ""
if sort {
order := descIndicator
if asc {
order = ascIndicator
}
return fmt.Sprintf("%s[%s::b]%s[::]", name, style.Header.SorterColor, order)
suffix = fmt.Sprintf("[%s::b]%s[::]", style.Header.SorterColor, order)
}
return displayName + suffix
}
func formatCell(field string, padding int) string {

View File

@ -30,7 +30,7 @@ func TestAliasNew(t *testing.T) {
require.NoError(t, v.Init(makeContext(t)))
assert.Equal(t, "Aliases", v.Name())
assert.Len(t, v.Hints(), 6)
assert.Len(t, v.Hints(), 7)
}
func TestAliasSearch(t *testing.T) {

View File

@ -17,5 +17,5 @@ func TestConfigMapNew(t *testing.T) {
require.NoError(t, s.Init(makeCtx(t)))
assert.Equal(t, "ConfigMaps", s.Name())
assert.Len(t, s.Hints(), 7)
assert.Len(t, s.Hints(), 8)
}

View File

@ -17,5 +17,5 @@ func TestContainerNew(t *testing.T) {
require.NoError(t, c.Init(makeCtx(t)))
assert.Equal(t, "Containers", c.Name())
assert.Len(t, c.Hints(), 19)
assert.Len(t, c.Hints(), 20)
}

View File

@ -17,5 +17,5 @@ func TestContext(t *testing.T) {
require.NoError(t, ctx.Init(makeCtx(t)))
assert.Equal(t, "Contexts", ctx.Name())
assert.Len(t, ctx.Hints(), 6)
assert.Len(t, ctx.Hints(), 7)
}

View File

@ -16,5 +16,5 @@ func TestDir(t *testing.T) {
require.NoError(t, v.Init(makeCtx(t)))
assert.Equal(t, "Directory", v.Name())
assert.Len(t, v.Hints(), 7)
assert.Len(t, v.Hints(), 8)
}

View File

@ -17,5 +17,5 @@ func TestDeploy(t *testing.T) {
require.NoError(t, v.Init(makeCtx(t)))
assert.Equal(t, "Deployments", v.Name())
assert.Len(t, v.Hints(), 17)
assert.Len(t, v.Hints(), 18)
}

View File

@ -17,5 +17,5 @@ func TestDaemonSet(t *testing.T) {
require.NoError(t, v.Init(makeCtx(t)))
assert.Equal(t, "DaemonSets", v.Name())
assert.Len(t, v.Hints(), 17)
assert.Len(t, v.Hints(), 18)
}

View File

@ -17,5 +17,5 @@ func TestPortForwardNew(t *testing.T) {
require.NoError(t, pf.Init(makeCtx(t)))
assert.Equal(t, "PortForwards", pf.Name())
assert.Len(t, pf.Hints(), 10)
assert.Len(t, pf.Hints(), 11)
}

View File

@ -17,5 +17,5 @@ func TestPriorityClassNew(t *testing.T) {
require.NoError(t, s.Init(makeCtx(t)))
assert.Equal(t, "PriorityClass", s.Name())
assert.Len(t, s.Hints(), 6)
assert.Len(t, s.Hints(), 7)
}

View File

@ -17,5 +17,5 @@ func TestRbacNew(t *testing.T) {
require.NoError(t, v.Init(makeCtx(t)))
assert.Equal(t, "Rbac", v.Name())
assert.Len(t, v.Hints(), 5)
assert.Len(t, v.Hints(), 6)
}

View File

@ -17,5 +17,5 @@ func TestReferenceNew(t *testing.T) {
require.NoError(t, s.Init(makeCtx(t)))
assert.Equal(t, "References", s.Name())
assert.Len(t, s.Hints(), 4)
assert.Len(t, s.Hints(), 5)
}

View File

@ -17,5 +17,5 @@ func TestScreenDumpNew(t *testing.T) {
require.NoError(t, po.Init(makeCtx(t)))
assert.Equal(t, "ScreenDumps", po.Name())
assert.Len(t, po.Hints(), 5)
assert.Len(t, po.Hints(), 6)
}

View File

@ -17,5 +17,5 @@ func TestSecretNew(t *testing.T) {
require.NoError(t, s.Init(makeCtx(t)))
assert.Equal(t, "Secrets", s.Name())
assert.Len(t, s.Hints(), 8)
assert.Len(t, s.Hints(), 9)
}

View File

@ -17,5 +17,5 @@ func TestStatefulSetNew(t *testing.T) {
require.NoError(t, s.Init(makeCtx(t)))
assert.Equal(t, "StatefulSets", s.Name())
assert.Len(t, s.Hints(), 14)
assert.Len(t, s.Hints(), 15)
}

View File

@ -174,5 +174,5 @@ func TestServiceNew(t *testing.T) {
require.NoError(t, s.Init(makeCtx(t)))
assert.Equal(t, "Services", s.Name())
assert.Len(t, s.Hints(), 12)
assert.Len(t, s.Hints(), 13)
}

View File

@ -95,6 +95,19 @@ func (t *Table) SendKey(evt *tcell.EventKey) {
func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
key := evt.Key()
// Handle Shift+Left/Right for column selection
if evt.Modifiers()&tcell.ModShift != 0 {
if key == tcell.KeyLeft {
t.Table.SelectPrevColumn()
return nil
}
if key == tcell.KeyRight {
t.Table.SelectNextColumn()
return nil
}
}
if key == tcell.KeyUp || key == tcell.KeyDown {
return evt
}
@ -214,6 +227,7 @@ func (t *Table) bindKeys() {
tcell.KeyCtrlW: ui.NewKeyAction("Toggle Wide", t.toggleWideCmd, false),
ui.KeyShiftN: ui.NewKeyAction("Sort Name", t.SortColCmd(nameCol, true), false),
ui.KeyShiftA: ui.NewKeyAction("Sort Age", t.SortColCmd(ageCol, true), false),
ui.KeyShiftS: ui.NewKeyAction("Sort Selected Column", t.sortSelectedColumnCmd, false),
})
}
@ -227,6 +241,11 @@ func (t *Table) toggleWideCmd(*tcell.EventKey) *tcell.EventKey {
return nil
}
func (t *Table) sortSelectedColumnCmd(*tcell.EventKey) *tcell.EventKey {
t.Table.SortSelectedColumn()
return nil
}
func (t *Table) cpCmd(evt *tcell.EventKey) *tcell.EventKey {
path := t.GetSelectedItem()
if path == "" {