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 testsmine
parent
2bf2f481ef
commit
6bc67e97f4
|
|
@ -39,6 +39,7 @@ type Table struct {
|
||||||
*SelectTable
|
*SelectTable
|
||||||
gvr *client.GVR
|
gvr *client.GVR
|
||||||
sortCol model1.SortColumn
|
sortCol model1.SortColumn
|
||||||
|
selectedColIdx int
|
||||||
manualSort bool
|
manualSort bool
|
||||||
Path string
|
Path string
|
||||||
Extras string
|
Extras string
|
||||||
|
|
@ -133,6 +134,137 @@ func (t *Table) getMSort() bool {
|
||||||
return t.manualSort
|
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.
|
// SetViewSetting sets custom view config is present.
|
||||||
func (t *Table) SetViewSetting(vs *config.ViewSetting) bool {
|
func (t *Table) SetViewSetting(vs *config.ViewSetting) bool {
|
||||||
t.mx.Lock()
|
t.mx.Lock()
|
||||||
|
|
@ -315,8 +447,17 @@ func (t *Table) doUpdate(data *model1.TableData) *model1.TableData {
|
||||||
} else {
|
} else {
|
||||||
t.actions.Delete(KeyShiftP)
|
t.actions.Delete(KeyShiftP)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oldSortCol := t.getSortCol()
|
||||||
t.setSortCol(data.ComputeSortCol(t.GetViewSetting(), t.getSortCol(), t.getMSort()))
|
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
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -429,6 +570,10 @@ func (t *Table) SortColCmd(name string, asc bool) func(evt *tcell.EventKey) *tce
|
||||||
sc.Name = name
|
sc.Name = name
|
||||||
t.setSortCol(sc)
|
t.setSortCol(sc)
|
||||||
t.setMSort(true)
|
t.setMSort(true)
|
||||||
|
|
||||||
|
// Sync selected column index with the new sort column
|
||||||
|
t.initSelectedColumn()
|
||||||
|
|
||||||
t.Refresh()
|
t.Refresh()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -486,8 +631,9 @@ func (t *Table) NameColIndex() int {
|
||||||
func (t *Table) AddHeaderCell(col int, h model1.HeaderColumn) {
|
func (t *Table) AddHeaderCell(col int, h model1.HeaderColumn) {
|
||||||
sc := t.getSortCol()
|
sc := t.getSortCol()
|
||||||
sortCol := h.Name == sc.Name
|
sortCol := h.Name == sc.Name
|
||||||
|
selectedCol := col == t.getSelectedColIdx()
|
||||||
styles := t.styles.Table()
|
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.SetExpansion(1)
|
||||||
c.SetSelectable(false)
|
c.SetSelectable(false)
|
||||||
c.SetAlign(h.Align)
|
c.SetAlign(h.Align)
|
||||||
|
|
|
||||||
|
|
@ -84,16 +84,25 @@ func SkinTitle(fmat string, style *config.Frame) string {
|
||||||
return fmat
|
return fmat
|
||||||
}
|
}
|
||||||
|
|
||||||
func sortIndicator(sort, asc bool, style *config.Table, name string) string {
|
func columnIndicator(sort, selected, asc bool, style *config.Table, name string) string {
|
||||||
if !sort {
|
// Build the column name with selection indicator
|
||||||
return name
|
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
|
order := descIndicator
|
||||||
if asc {
|
if asc {
|
||||||
order = ascIndicator
|
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 {
|
func formatCell(field string, padding int) string {
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ func TestAliasNew(t *testing.T) {
|
||||||
|
|
||||||
require.NoError(t, v.Init(makeContext(t)))
|
require.NoError(t, v.Init(makeContext(t)))
|
||||||
assert.Equal(t, "Aliases", v.Name())
|
assert.Equal(t, "Aliases", v.Name())
|
||||||
assert.Len(t, v.Hints(), 6)
|
assert.Len(t, v.Hints(), 7)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAliasSearch(t *testing.T) {
|
func TestAliasSearch(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@ func TestConfigMapNew(t *testing.T) {
|
||||||
|
|
||||||
require.NoError(t, s.Init(makeCtx(t)))
|
require.NoError(t, s.Init(makeCtx(t)))
|
||||||
assert.Equal(t, "ConfigMaps", s.Name())
|
assert.Equal(t, "ConfigMaps", s.Name())
|
||||||
assert.Len(t, s.Hints(), 7)
|
assert.Len(t, s.Hints(), 8)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@ func TestContainerNew(t *testing.T) {
|
||||||
|
|
||||||
require.NoError(t, c.Init(makeCtx(t)))
|
require.NoError(t, c.Init(makeCtx(t)))
|
||||||
assert.Equal(t, "Containers", c.Name())
|
assert.Equal(t, "Containers", c.Name())
|
||||||
assert.Len(t, c.Hints(), 19)
|
assert.Len(t, c.Hints(), 20)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@ func TestContext(t *testing.T) {
|
||||||
|
|
||||||
require.NoError(t, ctx.Init(makeCtx(t)))
|
require.NoError(t, ctx.Init(makeCtx(t)))
|
||||||
assert.Equal(t, "Contexts", ctx.Name())
|
assert.Equal(t, "Contexts", ctx.Name())
|
||||||
assert.Len(t, ctx.Hints(), 6)
|
assert.Len(t, ctx.Hints(), 7)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,5 +16,5 @@ func TestDir(t *testing.T) {
|
||||||
|
|
||||||
require.NoError(t, v.Init(makeCtx(t)))
|
require.NoError(t, v.Init(makeCtx(t)))
|
||||||
assert.Equal(t, "Directory", v.Name())
|
assert.Equal(t, "Directory", v.Name())
|
||||||
assert.Len(t, v.Hints(), 7)
|
assert.Len(t, v.Hints(), 8)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@ func TestDeploy(t *testing.T) {
|
||||||
|
|
||||||
require.NoError(t, v.Init(makeCtx(t)))
|
require.NoError(t, v.Init(makeCtx(t)))
|
||||||
assert.Equal(t, "Deployments", v.Name())
|
assert.Equal(t, "Deployments", v.Name())
|
||||||
assert.Len(t, v.Hints(), 17)
|
assert.Len(t, v.Hints(), 18)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@ func TestDaemonSet(t *testing.T) {
|
||||||
|
|
||||||
require.NoError(t, v.Init(makeCtx(t)))
|
require.NoError(t, v.Init(makeCtx(t)))
|
||||||
assert.Equal(t, "DaemonSets", v.Name())
|
assert.Equal(t, "DaemonSets", v.Name())
|
||||||
assert.Len(t, v.Hints(), 17)
|
assert.Len(t, v.Hints(), 18)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@ func TestPortForwardNew(t *testing.T) {
|
||||||
|
|
||||||
require.NoError(t, pf.Init(makeCtx(t)))
|
require.NoError(t, pf.Init(makeCtx(t)))
|
||||||
assert.Equal(t, "PortForwards", pf.Name())
|
assert.Equal(t, "PortForwards", pf.Name())
|
||||||
assert.Len(t, pf.Hints(), 10)
|
assert.Len(t, pf.Hints(), 11)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@ func TestPriorityClassNew(t *testing.T) {
|
||||||
|
|
||||||
require.NoError(t, s.Init(makeCtx(t)))
|
require.NoError(t, s.Init(makeCtx(t)))
|
||||||
assert.Equal(t, "PriorityClass", s.Name())
|
assert.Equal(t, "PriorityClass", s.Name())
|
||||||
assert.Len(t, s.Hints(), 6)
|
assert.Len(t, s.Hints(), 7)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@ func TestRbacNew(t *testing.T) {
|
||||||
|
|
||||||
require.NoError(t, v.Init(makeCtx(t)))
|
require.NoError(t, v.Init(makeCtx(t)))
|
||||||
assert.Equal(t, "Rbac", v.Name())
|
assert.Equal(t, "Rbac", v.Name())
|
||||||
assert.Len(t, v.Hints(), 5)
|
assert.Len(t, v.Hints(), 6)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@ func TestReferenceNew(t *testing.T) {
|
||||||
|
|
||||||
require.NoError(t, s.Init(makeCtx(t)))
|
require.NoError(t, s.Init(makeCtx(t)))
|
||||||
assert.Equal(t, "References", s.Name())
|
assert.Equal(t, "References", s.Name())
|
||||||
assert.Len(t, s.Hints(), 4)
|
assert.Len(t, s.Hints(), 5)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@ func TestScreenDumpNew(t *testing.T) {
|
||||||
|
|
||||||
require.NoError(t, po.Init(makeCtx(t)))
|
require.NoError(t, po.Init(makeCtx(t)))
|
||||||
assert.Equal(t, "ScreenDumps", po.Name())
|
assert.Equal(t, "ScreenDumps", po.Name())
|
||||||
assert.Len(t, po.Hints(), 5)
|
assert.Len(t, po.Hints(), 6)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@ func TestSecretNew(t *testing.T) {
|
||||||
|
|
||||||
require.NoError(t, s.Init(makeCtx(t)))
|
require.NoError(t, s.Init(makeCtx(t)))
|
||||||
assert.Equal(t, "Secrets", s.Name())
|
assert.Equal(t, "Secrets", s.Name())
|
||||||
assert.Len(t, s.Hints(), 8)
|
assert.Len(t, s.Hints(), 9)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@ func TestStatefulSetNew(t *testing.T) {
|
||||||
|
|
||||||
require.NoError(t, s.Init(makeCtx(t)))
|
require.NoError(t, s.Init(makeCtx(t)))
|
||||||
assert.Equal(t, "StatefulSets", s.Name())
|
assert.Equal(t, "StatefulSets", s.Name())
|
||||||
assert.Len(t, s.Hints(), 14)
|
assert.Len(t, s.Hints(), 15)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -174,5 +174,5 @@ func TestServiceNew(t *testing.T) {
|
||||||
|
|
||||||
require.NoError(t, s.Init(makeCtx(t)))
|
require.NoError(t, s.Init(makeCtx(t)))
|
||||||
assert.Equal(t, "Services", s.Name())
|
assert.Equal(t, "Services", s.Name())
|
||||||
assert.Len(t, s.Hints(), 12)
|
assert.Len(t, s.Hints(), 13)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,19 @@ func (t *Table) SendKey(evt *tcell.EventKey) {
|
||||||
|
|
||||||
func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
key := evt.Key()
|
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 {
|
if key == tcell.KeyUp || key == tcell.KeyDown {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
@ -214,6 +227,7 @@ func (t *Table) bindKeys() {
|
||||||
tcell.KeyCtrlW: ui.NewKeyAction("Toggle Wide", t.toggleWideCmd, false),
|
tcell.KeyCtrlW: ui.NewKeyAction("Toggle Wide", t.toggleWideCmd, false),
|
||||||
ui.KeyShiftN: ui.NewKeyAction("Sort Name", t.SortColCmd(nameCol, true), false),
|
ui.KeyShiftN: ui.NewKeyAction("Sort Name", t.SortColCmd(nameCol, true), false),
|
||||||
ui.KeyShiftA: ui.NewKeyAction("Sort Age", t.SortColCmd(ageCol, 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Table) sortSelectedColumnCmd(*tcell.EventKey) *tcell.EventKey {
|
||||||
|
t.Table.SortSelectedColumn()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Table) cpCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (t *Table) cpCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
path := t.GetSelectedItem()
|
path := t.GetSelectedItem()
|
||||||
if path == "" {
|
if path == "" {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue