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
|
|
@ -37,25 +37,26 @@ type (
|
|||
// Table represents tabular data.
|
||||
type Table struct {
|
||||
*SelectTable
|
||||
gvr *client.GVR
|
||||
sortCol model1.SortColumn
|
||||
manualSort bool
|
||||
Path string
|
||||
Extras string
|
||||
actions *KeyActions
|
||||
cmdBuff *model.FishBuff
|
||||
styles *config.Styles
|
||||
viewSetting *config.ViewSetting
|
||||
colorerFn model1.ColorerFunc
|
||||
decorateFn DecorateFunc
|
||||
wide bool
|
||||
toast bool
|
||||
hasMetrics bool
|
||||
ctx context.Context
|
||||
mx sync.RWMutex
|
||||
readOnly bool
|
||||
noIcon bool
|
||||
fullGVR bool
|
||||
gvr *client.GVR
|
||||
sortCol model1.SortColumn
|
||||
selectedColIdx int
|
||||
manualSort bool
|
||||
Path string
|
||||
Extras string
|
||||
actions *KeyActions
|
||||
cmdBuff *model.FishBuff
|
||||
styles *config.Styles
|
||||
viewSetting *config.ViewSetting
|
||||
colorerFn model1.ColorerFunc
|
||||
decorateFn DecorateFunc
|
||||
wide bool
|
||||
toast bool
|
||||
hasMetrics bool
|
||||
ctx context.Context
|
||||
mx sync.RWMutex
|
||||
readOnly bool
|
||||
noIcon bool
|
||||
fullGVR bool
|
||||
}
|
||||
|
||||
// NewTable returns a new table view.
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
order := descIndicator
|
||||
if asc {
|
||||
order = ascIndicator
|
||||
// Add sort indicator if this column is sorted
|
||||
suffix := ""
|
||||
if sort {
|
||||
order := descIndicator
|
||||
if asc {
|
||||
order = ascIndicator
|
||||
}
|
||||
suffix = fmt.Sprintf("[%s::b]%s[::]", style.Header.SorterColor, order)
|
||||
}
|
||||
return fmt.Sprintf("%s[%s::b]%s[::]", name, style.Header.SorterColor, order)
|
||||
|
||||
return displayName + suffix
|
||||
}
|
||||
|
||||
func formatCell(field string, padding int) string {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 == "" {
|
||||
|
|
|
|||
Loading…
Reference in New Issue