preliminary work on sort by columns

mine
derailed 2019-03-16 16:44:28 -06:00
parent fbb885d2f7
commit 16cf464c7a
20 changed files with 393 additions and 131 deletions

View File

@ -0,0 +1,29 @@
# Release v0.2.6
## Notes
Thank you to all that contributed with flushing out issues with K9s! I'll try
to mark some of these issues as fixed. But if you don't mind grab the latest
rev and see if we're happier with some of the fixes!
If you've filed an issue please help me verify and close.
Thank you so much for your support!!
---
## Change Logs
1. Preliminary drop on sorting by resource columns
2. Add sort by namespace, name and age for all views
3. Add invert sort functionality on all sortable views
4. Add sort on pod views for metrics and most other columns
5. For all other views we will add custom sort on a per request basis
---
## Resolved Bugs
+ [Issue #117](https://github.com/derailed/k9s/issues/117)
Was filtering out inactive ns which need to be there for all to see anyway!

View File

@ -30,7 +30,9 @@ func InList(ll []string, n string) bool {
func InNSList(nn []interface{}, ns string) bool { func InNSList(nn []interface{}, ns string) bool {
ss := make([]string, len(nn)) ss := make([]string, len(nn))
for i, n := range nn { for i, n := range nn {
ss[i] = n.(v1.Namespace).Name if nsp, ok := n.(v1.Namespace); ok {
ss[i] = nsp.Name
}
} }
return InList(ss, ns) return InList(ss, ns)
} }

View File

@ -222,7 +222,9 @@ func (c *Config) NamespaceNames() ([]string, error) {
} }
nn := make([]string, 0, len(ll)) nn := make([]string, 0, len(ll))
for _, n := range ll { for _, n := range ll {
nn = append(nn, n.(v1.Namespace).Name) if ns, ok := n.(v1.Namespace); ok {
nn = append(nn, ns.Name)
}
} }
return nn, nil return nn, nil
} }

View File

@ -1,7 +1,6 @@
package k8s package k8s
import ( import (
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
@ -31,10 +30,8 @@ func (*Namespace) List(_ string) (Collection, error) {
cc := make(Collection, len(rr.Items)) cc := make(Collection, len(rr.Items))
for i, r := range rr.Items { for i, r := range rr.Items {
if r.Status.Phase == v1.NamespaceActive {
cc[i] = r cc[i] = r
} }
}
return cc, nil return cc, nil
} }

View File

@ -89,16 +89,6 @@ func TestCMMarshal(t *testing.T) {
assert.Equal(t, cmYaml(), ma) assert.Equal(t, cmYaml(), ma)
} }
func TestCMListSort(t *testing.T) {
setup(t)
ca := NewMockCaller()
l := resource.NewConfigMapListWithArgs("blee", resource.NewConfigMapWithArgs(ca))
kk := []string{"c", "b", "a"}
l.SortFn()(kk)
assert.Equal(t, []string{"a", "b", "c"}, kk)
}
func TestCMListHasName(t *testing.T) { func TestCMListHasName(t *testing.T) {
setup(t) setup(t)

View File

@ -52,16 +52,6 @@ func TestCTXDelete(t *testing.T) {
ca.VerifyWasCalledOnce().Delete("", "fred") ca.VerifyWasCalledOnce().Delete("", "fred")
} }
func TestCTXListSort(t *testing.T) {
setup(t)
ca := NewMockSwitchableRes()
l := resource.NewContextListWithArgs("blee", resource.NewContextWithArgs(ca))
kk := []string{"c", "b", "a"}
l.SortFn()(kk)
assert.Equal(t, []string{"a", "b", "c"}, kk)
}
func TestCTXListHasName(t *testing.T) { func TestCTXListHasName(t *testing.T) {
setup(t) setup(t)

View File

@ -2,7 +2,6 @@ package resource
import ( import (
"reflect" "reflect"
"sort"
"github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/k8s"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
@ -34,9 +33,6 @@ const (
) )
type ( type (
// SortFn provides for sorting items in list.
SortFn func([]string)
// RowEvent represents a call for action after a resource reconciliation. // RowEvent represents a call for action after a resource reconciliation.
// Tracks whether a resource got added, deleted or updated. // Tracks whether a resource got added, deleted or updated.
RowEvent struct { RowEvent struct {
@ -71,7 +67,6 @@ type (
GetName() string GetName() string
Access(flag int) bool Access(flag int) bool
HasXRay() bool HasXRay() bool
SortFn() SortFn
} }
// Columnar tracks resources that can be diplayed in a tabular fashion. // Columnar tracks resources that can be diplayed in a tabular fashion.
@ -85,6 +80,9 @@ type (
// Row represents a collection of string fields. // Row represents a collection of string fields.
Row []string Row []string
// Rows represents a collection of rows.
Rows []Row
// Columnars a collection of columnars. // Columnars a collection of columnars.
Columnars []Columnar Columnars []Columnar
@ -112,7 +110,6 @@ type (
xray bool xray bool
api Resource api Resource
cache RowEvents cache RowEvents
sortFn func([]string)
} }
) )
@ -130,13 +127,6 @@ func newList(ns, name string, api Resource, v int) *list {
} }
} }
func (l *list) SortFn() SortFn {
if l.sortFn == nil {
return sort.Strings
}
return l.sortFn
}
func (l *list) HasXRay() bool { func (l *list) HasXRay() bool {
return l.xray return l.xray
} }

View File

@ -90,16 +90,6 @@ func TestSecretMarshal(t *testing.T) {
assert.Equal(t, secretYaml(), ma) assert.Equal(t, secretYaml(), ma)
} }
func TestSecretListSort(t *testing.T) {
setup(t)
ca := NewMockCaller()
l := resource.NewSecretListWithArgs("blee", resource.NewSecretWithArgs(ca))
kk := []string{"c", "b", "a"}
l.SortFn()(kk)
assert.Equal(t, []string{"a", "b", "c"}, kk)
}
func TestSecretListHasName(t *testing.T) { func TestSecretListHasName(t *testing.T) {
setup(t) setup(t)

View File

@ -3,7 +3,6 @@ package views
import ( import (
"context" "context"
"fmt" "fmt"
"sort"
"strings" "strings"
"time" "time"
@ -25,17 +24,18 @@ type aliasView struct {
} }
func newAliasView(app *appView) *aliasView { func newAliasView(app *appView) *aliasView {
v := aliasView{tableView: newTableView(app, aliasTitle, nil)} v := aliasView{tableView: newTableView(app, aliasTitle)}
{ {
v.SetSelectedStyle(tcell.ColorWhite, tcell.ColorFuchsia, tcell.AttrNone) v.SetSelectedStyle(tcell.ColorWhite, tcell.ColorFuchsia, tcell.AttrNone)
v.colorerFn = aliasColorer v.colorerFn = aliasColorer
v.current = app.content.GetPrimitive("main").(igniter) v.current = app.content.GetPrimitive("main").(igniter)
v.sortFn = v.sorterFn
v.currentNS = "" v.currentNS = ""
} }
v.actions[tcell.KeyEnter] = newKeyAction("Goto", v.gotoCmd, true) v.actions[tcell.KeyEnter] = newKeyAction("Goto", v.gotoCmd, true)
v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd, false) v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd, false)
v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd, false) v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd, false)
v.actions[KeyShiftR] = newKeyAction("Sort Resources", v.sortResourceCmd, true)
v.actions[KeyShiftG] = newKeyAction("Sort Groups", v.sortGroupCmd, true)
ctx, cancel := context.WithCancel(context.TODO()) ctx, cancel := context.WithCancel(context.TODO())
v.cancel = cancel v.cancel = cancel
@ -55,10 +55,6 @@ func newAliasView(app *appView) *aliasView {
return &v return &v
} }
func (v *aliasView) sorterFn(ss []string) {
sort.Strings(ss)
}
// Init the view. // Init the view.
func (v *aliasView) init(context.Context, string) { func (v *aliasView) init(context.Context, string) {
v.update(v.hydrate()) v.update(v.hydrate())
@ -70,6 +66,18 @@ func (v *aliasView) getTitle() string {
return aliasTitle return aliasTitle
} }
func (v *aliasView) sortResourceCmd(evt *tcell.EventKey) *tcell.EventKey {
v.sortCol.index, v.sortCol.asc = 1, true
v.refresh()
return nil
}
func (v *aliasView) sortGroupCmd(evt *tcell.EventKey) *tcell.EventKey {
v.sortCol.index, v.sortCol.asc = 2, true
v.refresh()
return nil
}
func (v *aliasView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { func (v *aliasView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.cmdBuff.empty() { if !v.cmdBuff.empty() {
v.cmdBuff.reset() v.cmdBuff.reset()
@ -123,9 +131,9 @@ func (v *aliasView) hydrate() resource.TableData {
cmds := helpCmds() cmds := helpCmds()
data := resource.TableData{ data := resource.TableData{
Header: resource.Row{"ALIAS", "RESOURCE", "APIGROUP"}, Header: resource.Row{"NAME", "RESOURCE", "APIGROUP"},
Rows: make(resource.RowEvents, len(cmds)), Rows: make(resource.RowEvents, len(cmds)),
Namespace: "", Namespace: resource.NotNamespaced,
} }
for k := range cmds { for k := range cmds {

View File

@ -97,7 +97,7 @@ func (a *appView) Init(v string, rate int, flags *genericclioptions.ConfigFlags)
header := tview.NewFlex() header := tview.NewFlex()
{ {
header.SetDirection(tview.FlexColumn) header.SetDirection(tview.FlexColumn)
header.AddItem(a.clusterInfoView, 55, 1, false) header.AddItem(a.clusterInfoView, 35, 1, false)
header.AddItem(a.menuView, 0, 1, false) header.AddItem(a.menuView, 0, 1, false)
header.AddItem(logoView(), 26, 1, false) header.AddItem(logoView(), 26, 1, false)
} }

View File

@ -33,12 +33,12 @@ func (v *clusterInfoView) init() {
v.SetCell(row, 1, v.infoCell(cluster.UserName())) v.SetCell(row, 1, v.infoCell(cluster.UserName()))
row++ row++
v.SetCell(row, 0, v.sectionCell("K9s Version")) v.SetCell(row, 0, v.sectionCell("K9s Rev"))
v.SetCell(row, 1, v.infoCell(v.app.version)) v.SetCell(row, 1, v.infoCell(v.app.version))
row++ row++
rev := cluster.Version() rev := cluster.Version()
v.SetCell(row, 0, v.sectionCell("K8s Version")) v.SetCell(row, 0, v.sectionCell("K8s Rev"))
v.SetCell(row, 1, v.infoCell(rev)) v.SetCell(row, 1, v.infoCell(rev))
row++ row++

View File

@ -16,6 +16,7 @@ func newContextView(t string, app *appView, list resource.List, c colorerFn) res
v := contextView{newResourceView(t, app, list, c).(*resourceView)} v := contextView{newResourceView(t, app, list, c).(*resourceView)}
{ {
v.extraActionsFn = v.extraActions v.extraActionsFn = v.extraActions
v.getTV().cleanseFn = v.cleanser
v.switchPage("ctx") v.switchPage("ctx")
} }
return &v return &v
@ -39,15 +40,19 @@ func (v *contextView) useCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil return nil
} }
func (v *contextView) useContext(name string) error { func (*contextView) cleanser(s string) string {
ctx := strings.TrimSpace(name) name := strings.TrimSpace(s)
if strings.HasSuffix(ctx, "*") { if strings.HasSuffix(name, "*") {
ctx = strings.TrimRight(ctx, "*") name = strings.TrimRight(name, "*")
} }
if strings.HasSuffix(ctx, "(𝜟)") { if strings.HasSuffix(name, "(𝜟)") {
ctx = strings.TrimRight(ctx, "(𝜟)") name = strings.TrimRight(name, "(𝜟)")
} }
return name
}
func (v *contextView) useContext(name string) error {
ctx := v.cleanser(name)
if err := v.list.Resource().(*resource.Context).Switch(ctx); err != nil { if err := v.list.Resource().(*resource.Context).Switch(ctx); err != nil {
return err return err
} }
@ -59,5 +64,6 @@ func (v *contextView) useContext(name string) error {
if tv, ok := v.GetPrimitive("ctx").(*tableView); ok { if tv, ok := v.GetPrimitive("ctx").(*tableView); ok {
tv.Select(0, 0) tv.Select(0, 0)
} }
return nil return nil
} }

View File

@ -88,8 +88,7 @@ func (v *helpView) init(_ context.Context, _ string) {
{"k", "Up"}, {"k", "Up"},
{"j", "Down"}, {"j", "Down"},
} }
fmt.Fprintln(v) fmt.Fprintf(v, "\n🤖 [aqua::b]%s\n", "View Navigation")
fmt.Fprintf(v, "🖲 [aqua::b]%s\n", "View Navigation")
for _, h := range navigation { for _, h := range navigation {
fmt.Fprintf(v, "[pink::b]%9s [gray::]%s\n", h.key, h.description) fmt.Fprintf(v, "[pink::b]%9s [gray::]%s\n", h.key, h.description)
} }
@ -98,8 +97,7 @@ func (v *helpView) init(_ context.Context, _ string) {
{"?", "Help"}, {"?", "Help"},
{"a", "Aliases view"}, {"a", "Aliases view"},
} }
fmt.Fprintln(v) fmt.Fprintf(v, "\n😱 [aqua::b]%s\n", "Help")
fmt.Fprintf(v, "️️⁉️ [aqua::b]%s\n", "Help")
for _, h := range views { for _, h := range views {
fmt.Fprintf(v, "[pink::b]%9s [gray::]%s\n", h.key, h.description) fmt.Fprintf(v, "[pink::b]%9s [gray::]%s\n", h.key, h.description)
} }

View File

@ -128,15 +128,15 @@ func (v *menuView) buildMenuTable(hh hints) [][]string {
} }
var row, col int var row, col int
firstNS, firstCmd := true, true firstCmd := true
maxKeys := make([]int, colCount+1) maxKeys := make([]int, colCount+1)
for _, h := range hh { for _, h := range hh {
isDigit := menuRX.MatchString(h.mnemonic) isDigit := menuRX.MatchString(h.mnemonic)
if isDigit && firstNS { // if isDigit && firstNS {
row, col, firstNS = 0, col+1, false // row, col, firstNS = 0, 2, false
} // }
if !isDigit && firstCmd { if !isDigit && firstCmd {
row, col, firstCmd = 0, 0, false row, col, firstCmd = 0, col+1, false
} }
if maxKeys[col] < len(h.mnemonic) { if maxKeys[col] < len(h.mnemonic) {
maxKeys[col] = len(h.mnemonic) maxKeys[col] = len(h.mnemonic)
@ -158,6 +158,7 @@ func (v *menuView) buildMenuTable(hh hints) [][]string {
strTable[row][col] = v.formatMenu(table[row][col], maxKeys[col]) strTable[row][col] = v.formatMenu(table[row][col], maxKeys[col])
} }
} }
return strTable return strTable
} }
@ -165,6 +166,7 @@ func (*menuView) toMnemonic(s string) string {
if len(s) == 0 { if len(s) == 0 {
return s return s
} }
return "<" + strings.ToLower(s) + ">" return "<" + strings.ToLower(s) + ">"
} }
@ -173,6 +175,7 @@ func (v *menuView) formatMenu(h hint, size int) string {
if err == nil { if err == nil {
return fmt.Sprintf(menuIndexFmt, i, resource.Truncate(h.description, 14)) return fmt.Sprintf(menuIndexFmt, i, resource.Truncate(h.description, 14))
} }
menuFmt := " [dodgerblue::b]%-" + strconv.Itoa(size+2) + "s [white::d]%s " menuFmt := " [dodgerblue::b]%-" + strconv.Itoa(size+2) + "s [white::d]%s "
return fmt.Sprintf(menuFmt, v.toMnemonic(h.mnemonic), h.description) return fmt.Sprintf(menuFmt, v.toMnemonic(h.mnemonic), h.description)
} }

View File

@ -27,6 +27,7 @@ func newNamespaceView(t string, app *appView, list resource.List, c colorerFn) r
v.extraActionsFn = v.extraActions v.extraActionsFn = v.extraActions
v.selectedFn = v.getSelectedItem v.selectedFn = v.getSelectedItem
v.decorateDataFn = v.decorate v.decorateDataFn = v.decorate
v.getTV().cleanseFn = v.cleanser
v.switchPage("ns") v.switchPage("ns")
return &v return &v
} }

View File

@ -143,6 +143,23 @@ func (v *podView) showLogs(path, co string, previous bool) {
func (v *podView) extraActions(aa keyActions) { func (v *podView) extraActions(aa keyActions) {
aa[KeyL] = newKeyAction("Logs", v.logsCmd, true) aa[KeyL] = newKeyAction("Logs", v.logsCmd, true)
aa[KeyS] = newKeyAction("Shell", v.shellCmd, true) aa[KeyS] = newKeyAction("Shell", v.shellCmd, true)
aa[KeyShiftR] = newKeyAction("Sort Ready", v.sortColCmd(1, false), true)
aa[KeyShiftS] = newKeyAction("Sort Status", v.sortColCmd(2, true), true)
aa[KeyShiftT] = newKeyAction("Sort Restart", v.sortColCmd(3, false), true)
aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(4, false), true)
aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(5, false), true)
aa[KeyShiftO] = newKeyAction("Sort Node", v.sortColCmd(7, true), true)
aa[KeyShiftQ] = newKeyAction("Sort QOS", v.sortColCmd(8, true), true)
}
func (v *podView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey {
t := v.getTV()
t.sortCol.index, t.sortCol.asc = t.nameColIndex()+col, asc
t.refresh()
return nil
}
} }
func fetchContainers(l resource.List, po string, includeInit bool) ([]string, error) { func fetchContainers(l resource.List, po string, includeInit bool) ([]string, error) {

View File

@ -73,7 +73,7 @@ func resourceViews() map[string]resCmd {
return map[string]resCmd{ return map[string]resCmd{
"cm": { "cm": {
title: "ConfigMaps", title: "ConfigMaps",
api: "core", api: "",
viewFn: newResourceView, viewFn: newResourceView,
listFn: resource.NewConfigMapList, listFn: resource.NewConfigMapList,
colorerFn: defaultColorer, colorerFn: defaultColorer,
@ -108,14 +108,14 @@ func resourceViews() map[string]resCmd {
}, },
"ctx": { "ctx": {
title: "Contexts", title: "Contexts",
api: "core", api: "",
viewFn: newContextView, viewFn: newContextView,
listFn: resource.NewContextList, listFn: resource.NewContextList,
colorerFn: ctxColorer, colorerFn: ctxColorer,
}, },
"ds": { "ds": {
title: "DaemonSets", title: "DaemonSets",
api: "core", api: "",
viewFn: newResourceView, viewFn: newResourceView,
listFn: resource.NewDaemonSetList, listFn: resource.NewDaemonSetList,
colorerFn: dpColorer, colorerFn: dpColorer,
@ -129,14 +129,14 @@ func resourceViews() map[string]resCmd {
}, },
"ep": { "ep": {
title: "EndPoints", title: "EndPoints",
api: "core", api: "",
viewFn: newResourceView, viewFn: newResourceView,
listFn: resource.NewEndpointsList, listFn: resource.NewEndpointsList,
colorerFn: defaultColorer, colorerFn: defaultColorer,
}, },
"ev": { "ev": {
title: "Events", title: "Events",
api: "core", api: "",
viewFn: newResourceView, viewFn: newResourceView,
listFn: resource.NewEventList, listFn: resource.NewEventList,
colorerFn: evColorer, colorerFn: evColorer,
@ -164,35 +164,35 @@ func resourceViews() map[string]resCmd {
}, },
"no": { "no": {
title: "Nodes", title: "Nodes",
api: "core", api: "",
viewFn: newResourceView, viewFn: newResourceView,
listFn: resource.NewNodeList, listFn: resource.NewNodeList,
colorerFn: nsColorer, colorerFn: nsColorer,
}, },
"ns": { "ns": {
title: "Namespaces", title: "Namespaces",
api: "core", api: "",
viewFn: newNamespaceView, viewFn: newNamespaceView,
listFn: resource.NewNamespaceList, listFn: resource.NewNamespaceList,
colorerFn: nsColorer, colorerFn: nsColorer,
}, },
"po": { "po": {
title: "Pods", title: "Pods",
api: "core", api: "",
viewFn: newPodView, viewFn: newPodView,
listFn: resource.NewPodList, listFn: resource.NewPodList,
colorerFn: podColorer, colorerFn: podColorer,
}, },
"pv": { "pv": {
title: "PersistentVolumes", title: "PersistentVolumes",
api: "core", api: "",
viewFn: newResourceView, viewFn: newResourceView,
listFn: resource.NewPVList, listFn: resource.NewPVList,
colorerFn: pvColorer, colorerFn: pvColorer,
}, },
"pvc": { "pvc": {
title: "PersistentVolumeClaims", title: "PersistentVolumeClaims",
api: "core", api: "",
viewFn: newResourceView, viewFn: newResourceView,
listFn: resource.NewPVCList, listFn: resource.NewPVCList,
colorerFn: pvcColorer, colorerFn: pvcColorer,
@ -206,7 +206,7 @@ func resourceViews() map[string]resCmd {
}, },
"rc": { "rc": {
title: "ReplicationControllers", title: "ReplicationControllers",
api: "v1", api: "",
viewFn: newResourceView, viewFn: newResourceView,
listFn: resource.NewReplicationControllerList, listFn: resource.NewReplicationControllerList,
colorerFn: rsColorer, colorerFn: rsColorer,
@ -227,14 +227,14 @@ func resourceViews() map[string]resCmd {
}, },
"sa": { "sa": {
title: "ServiceAccounts", title: "ServiceAccounts",
api: "core", api: "",
viewFn: newResourceView, viewFn: newResourceView,
listFn: resource.NewServiceAccountList, listFn: resource.NewServiceAccountList,
colorerFn: defaultColorer, colorerFn: defaultColorer,
}, },
"sec": { "sec": {
title: "Secrets", title: "Secrets",
api: "core", api: "",
viewFn: newResourceView, viewFn: newResourceView,
listFn: resource.NewSecretList, listFn: resource.NewSecretList,
colorerFn: defaultColorer, colorerFn: defaultColorer,
@ -248,7 +248,7 @@ func resourceViews() map[string]resCmd {
}, },
"svc": { "svc": {
title: "Services", title: "Services",
api: "core", api: "",
viewFn: newResourceView, viewFn: newResourceView,
listFn: resource.NewServiceList, listFn: resource.NewServiceList,
colorerFn: defaultColorer, colorerFn: defaultColorer,

View File

@ -57,7 +57,7 @@ func newResourceView(title string, app *appView, list resource.List, c colorerFn
Pages: tview.NewPages(), Pages: tview.NewPages(),
} }
tv := newTableView(app, v.title, list.SortFn()) tv := newTableView(app, v.title)
{ {
tv.SetColorer(c) tv.SetColorer(c)
tv.SetSelectionChangedFunc(v.selChanged) tv.SetSelectionChangedFunc(v.selChanged)

88
internal/views/sorters.go Normal file
View File

@ -0,0 +1,88 @@
package views
import (
"regexp"
"strconv"
"strings"
"github.com/derailed/k9s/internal/resource"
)
type rowSorter struct {
rows resource.Rows
index int
asc bool
}
func (s rowSorter) Len() int {
return len(s.rows)
}
func (s rowSorter) Swap(i, j int) {
s.rows[i], s.rows[j] = s.rows[j], s.rows[i]
}
func (s rowSorter) Less(i, j int) bool {
c1 := s.rows[i][s.index]
c2 := s.rows[j][s.index]
if m1, ok := isMetric(c1); ok {
m2, _ := isMetric(c2)
i1, _ := strconv.Atoi(m1)
i2, _ := strconv.Atoi(m2)
if s.asc {
return i1 < i2
}
return i1 > i2
}
c := strings.Compare(c1, c2)
if s.asc {
return c < 0
}
return c > 0
}
// ----------------------------------------------------------------------------
type groupSorter struct {
rows []string
asc bool
}
func (s groupSorter) Len() int {
return len(s.rows)
}
func (s groupSorter) Swap(i, j int) {
s.rows[i], s.rows[j] = s.rows[j], s.rows[i]
}
func (s groupSorter) Less(i, j int) bool {
c1 := s.rows[i]
c2 := s.rows[j]
if m1, ok := isMetric(c1); ok {
m2, _ := isMetric(c2)
i1, _ := strconv.Atoi(m1)
i2, _ := strconv.Atoi(m2)
if s.asc {
return i1 < i2
}
return i1 > i2
}
c := strings.Compare(c1, c2)
if s.asc {
return c < 0
}
return c > 0
}
// ----------------------------------------------------------------------------
// Helpers...
var metricRX = regexp.MustCompile(`\A(\d+)(m|Mi)\z`)
func isMetric(s string) (string, bool) {
if m := metricRX.FindStringSubmatch(s); len(m) == 3 {
return m[1], true
}
return s, false
}

View File

@ -3,6 +3,7 @@ package views
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"sort"
"strings" "strings"
"sync" "sync"
@ -19,6 +20,15 @@ const (
) )
type ( type (
sortFn func(rows resource.Rows, sortCol sortColumn)
cleanseFn func(string) string
sortColumn struct {
index int
colCount int
asc bool
}
tableView struct { tableView struct {
*tview.Table *tview.Table
@ -28,18 +38,20 @@ type (
refreshMX sync.Mutex refreshMX sync.Mutex
actions keyActions actions keyActions
colorerFn colorerFn colorerFn colorerFn
sortFn resource.SortFn sortFn sortFn
cleanseFn cleanseFn
data resource.TableData data resource.TableData
cmdBuff *cmdBuff cmdBuff *cmdBuff
sortBuff *cmdBuff
tableMX sync.Mutex tableMX sync.Mutex
sortCol sortColumn
} }
) )
func newTableView(app *appView, title string, sortFn resource.SortFn) *tableView { func newTableView(app *appView, title string) *tableView {
v := tableView{app: app, Table: tview.NewTable()} v := tableView{app: app, Table: tview.NewTable(), sortCol: sortColumn{0, 0, true}}
{ {
v.baseTitle = title v.baseTitle = title
v.sortFn = sortFn
v.actions = make(keyActions) v.actions = make(keyActions)
v.SetBorder(true) v.SetBorder(true)
v.SetBorderColor(tcell.ColorDodgerBlue) v.SetBorderColor(tcell.ColorDodgerBlue)
@ -53,9 +65,14 @@ func newTableView(app *appView, title string, sortFn resource.SortFn) *tableView
v.SetInputCapture(v.keyboard) v.SetInputCapture(v.keyboard)
} }
v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd, false) v.actions[KeyShiftI] = newKeyAction("Invert", v.sortInvertCmd, true)
v.actions[tcell.KeyEnter] = newKeyAction("Search", v.filterCmd, false) v.actions[KeyShiftN] = newKeyAction("Sort Name", v.sortColCmd(0), true)
v.actions[tcell.KeyEscape] = newKeyAction("Reset Filter", v.resetCmd, false) v.actions[KeyShiftA] = newKeyAction("Sort Age", v.sortColCmd(-1), true)
v.actions[KeySlash] = newKeyAction("Filter Mode", v.activateCmd, false)
v.actions[tcell.KeyEscape] = newKeyAction("Filter Reset", v.resetCmd, false)
v.actions[tcell.KeyEnter] = newKeyAction("Filter", v.filterCmd, false)
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false) v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[KeyG] = newKeyAction("Top", app.puntCmd, false) v.actions[KeyG] = newKeyAction("Top", app.puntCmd, false)
v.actions[KeyShiftG] = newKeyAction("Bottom", app.puntCmd, false) v.actions[KeyShiftG] = newKeyAction("Bottom", app.puntCmd, false)
@ -128,13 +145,45 @@ func (v *tableView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil return nil
} }
func (v *tableView) nameColIndex() int {
col := 0
if v.currentNS == resource.AllNamespaces {
col++
}
return col
}
func (v *tableView) sortColCmd(col int) func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey {
if col == -1 {
v.sortCol.index, v.sortCol.asc = v.GetColumnCount()-1, true
} else {
v.sortCol.index, v.sortCol.asc = v.nameColIndex()+col, true
}
v.refresh()
return nil
}
}
func (v *tableView) sortNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
v.sortCol.index, v.sortCol.asc = 0, true
v.refresh()
return nil
}
func (v *tableView) sortInvertCmd(evt *tcell.EventKey) *tcell.EventKey {
v.sortCol.asc = !v.sortCol.asc
v.refresh()
return nil
}
func (v *tableView) activateCmd(evt *tcell.EventKey) *tcell.EventKey { func (v *tableView) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.app.cmdView.inCmdMode() { if v.app.cmdView.inCmdMode() {
return evt return evt
} }
v.app.flash(flashInfo, "Filtering...") v.app.flash(flashInfo, "Filtering...")
log.Info().Msg("Entering filtering mode...")
v.cmdBuff.reset() v.cmdBuff.reset()
v.cmdBuff.setActive(true) v.cmdBuff.setActive(true)
return nil return nil
@ -217,49 +266,151 @@ func (v *tableView) filtered() resource.TableData {
return filtered return filtered
} }
func (v *tableView) displayCol(index int, name string) string {
if v.sortCol.index != index {
return name
}
order := "↓"
if v.sortCol.asc {
order = "↑"
}
return fmt.Sprintf("%s[green::]%s[::]", name, order)
}
func (v *tableView) doUpdate(data resource.TableData) { func (v *tableView) doUpdate(data resource.TableData) {
v.Clear()
v.currentNS = data.Namespace v.currentNS = data.Namespace
if v.currentNS == resource.AllNamespaces {
v.actions[KeyShiftG] = newKeyAction("Sort Namespace", v.sortNamespaceCmd, true)
} else {
delete(v.actions, KeyShiftS)
}
v.Clear()
// Going from namespace to non namespace or vice-versa?
switch {
case v.sortCol.colCount == 0:
case len(data.Header) > v.sortCol.colCount:
v.sortCol.index++
case len(data.Header) < v.sortCol.colCount:
v.sortCol.index--
}
v.sortCol.colCount = len(data.Header)
if v.sortCol.index < 0 {
v.sortCol.index = 0
}
var row int var row int
for col, h := range data.Header { for col, h := range data.Header {
c := tview.NewTableCell(h) v.addHeaderCell(col, h)
{
c.SetExpansion(3)
if len(h) == 0 {
c.SetExpansion(1)
}
c.SetTextColor(tcell.ColorWhite)
}
v.SetCell(row, col, c)
} }
row++ row++
keys := make([]string, 0, len(data.Rows)) keys := v.sortRows(data)
for k := range data.Rows { groupKeys := map[string][]string{}
keys = append(keys, k)
}
if v.sortFn != nil {
v.sortFn(keys)
}
for _, k := range keys { for _, k := range keys {
grp := data.Rows[k].Fields[v.sortCol.index]
if s, ok := groupKeys[grp]; ok {
s = append(s, k)
groupKeys[grp] = s
} else {
groupKeys[grp] = []string{k}
}
}
// Performs secondary to sort by name for each groups.
gKeys := make([]string, len(keys))
for k, v := range groupKeys {
sort.Strings(v)
gKeys = append(gKeys, k)
}
rs := groupSorter{gKeys, v.sortCol.asc}
sort.Sort(rs)
for _, gk := range gKeys {
for _, k := range groupKeys[gk] {
fgColor := tcell.ColorGray fgColor := tcell.ColorGray
if v.colorerFn != nil { if v.colorerFn != nil {
fgColor = v.colorerFn(data.Namespace, data.Rows[k]) fgColor = v.colorerFn(data.Namespace, data.Rows[k])
} }
for col, f := range data.Rows[k].Fields { for col, field := range data.Rows[k].Fields {
c := tview.NewTableCell(deltas(data.Rows[k].Deltas[col], f)) v.addBodyCell(row, col, field, data.Rows[k].Deltas[col], fgColor)
{
c.SetExpansion(3)
if len(data.Header[col]) == 0 {
c.SetExpansion(1)
}
c.SetTextColor(fgColor)
}
v.SetCell(row, col, c)
} }
row++ row++
} }
}
}
func (v *tableView) addHeaderCell(col int, name string) {
c := tview.NewTableCell(v.displayCol(col, name))
{
c.SetExpansion(3)
if len(name) == 0 {
c.SetExpansion(1)
}
c.SetTextColor(tcell.ColorAntiqueWhite)
}
v.SetCell(0, col, c)
}
func (v *tableView) addBodyCell(row, col int, field, delta string, color tcell.Color) {
c := tview.NewTableCell(deltas(delta, field))
{
c.SetExpansion(3)
if len(v.GetCell(0, col).Text) == 0 {
c.SetExpansion(1)
}
c.SetTextColor(color)
}
v.SetCell(row, col, c)
}
func (v *tableView) defaultSort(rows resource.Rows) {
t := rowSorter{rows: rows, index: v.sortCol.index, asc: v.sortCol.asc}
sort.Sort(t)
}
func (v *tableView) sortRows(data resource.TableData) []string {
rows := make(resource.Rows, 0, len(data.Rows))
for _, r := range data.Rows {
rows = append(rows, r.Fields)
}
if v.sortFn != nil {
v.sortFn(rows, v.sortCol)
} else {
v.defaultSort(rows)
}
keys := make([]string, len(rows))
for i, r := range rows {
col, prefix := 0, v.currentNS
switch v.currentNS {
case resource.AllNamespaces:
col, prefix = 1, r[0]
case resource.NotNamespaced:
prefix = ""
}
key := r[col]
if v.cleanseFn != nil {
key = v.cleanseFn(key)
} else {
key = v.defaultColCleanse(key)
}
if len(prefix) == 0 {
keys[i] = key
} else {
keys[i] = prefix + "/" + key
}
}
return keys
}
func (*tableView) defaultColCleanse(s string) string {
return strings.TrimSpace(s)
} }
func (v *tableView) resetTitle() { func (v *tableView) resetTitle() {