preliminary work on sort by columns
parent
fbb885d2f7
commit
16cf464c7a
|
|
@ -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!
|
||||
|
|
@ -30,7 +30,9 @@ func InList(ll []string, n string) bool {
|
|||
func InNSList(nn []interface{}, ns string) bool {
|
||||
ss := make([]string, len(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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -222,7 +222,9 @@ func (c *Config) NamespaceNames() ([]string, error) {
|
|||
}
|
||||
nn := make([]string, 0, len(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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package k8s
|
||||
|
||||
import (
|
||||
v1 "k8s.io/api/core/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))
|
||||
for i, r := range rr.Items {
|
||||
if r.Status.Phase == v1.NamespaceActive {
|
||||
cc[i] = r
|
||||
}
|
||||
}
|
||||
|
||||
return cc, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,16 +89,6 @@ func TestCMMarshal(t *testing.T) {
|
|||
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) {
|
||||
setup(t)
|
||||
|
||||
|
|
|
|||
|
|
@ -52,16 +52,6 @@ func TestCTXDelete(t *testing.T) {
|
|||
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) {
|
||||
setup(t)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package resource
|
|||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
|
|
@ -34,9 +33,6 @@ const (
|
|||
)
|
||||
|
||||
type (
|
||||
// SortFn provides for sorting items in list.
|
||||
SortFn func([]string)
|
||||
|
||||
// RowEvent represents a call for action after a resource reconciliation.
|
||||
// Tracks whether a resource got added, deleted or updated.
|
||||
RowEvent struct {
|
||||
|
|
@ -71,7 +67,6 @@ type (
|
|||
GetName() string
|
||||
Access(flag int) bool
|
||||
HasXRay() bool
|
||||
SortFn() SortFn
|
||||
}
|
||||
|
||||
// Columnar tracks resources that can be diplayed in a tabular fashion.
|
||||
|
|
@ -85,6 +80,9 @@ type (
|
|||
// Row represents a collection of string fields.
|
||||
Row []string
|
||||
|
||||
// Rows represents a collection of rows.
|
||||
Rows []Row
|
||||
|
||||
// Columnars a collection of columnars.
|
||||
Columnars []Columnar
|
||||
|
||||
|
|
@ -112,7 +110,6 @@ type (
|
|||
xray bool
|
||||
api Resource
|
||||
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 {
|
||||
return l.xray
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,16 +90,6 @@ func TestSecretMarshal(t *testing.T) {
|
|||
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) {
|
||||
setup(t)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package views
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -25,17 +24,18 @@ type aliasView struct {
|
|||
}
|
||||
|
||||
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.colorerFn = aliasColorer
|
||||
v.current = app.content.GetPrimitive("main").(igniter)
|
||||
v.sortFn = v.sorterFn
|
||||
v.currentNS = ""
|
||||
}
|
||||
v.actions[tcell.KeyEnter] = newKeyAction("Goto", v.gotoCmd, true)
|
||||
v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd, 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())
|
||||
v.cancel = cancel
|
||||
|
|
@ -55,10 +55,6 @@ func newAliasView(app *appView) *aliasView {
|
|||
return &v
|
||||
}
|
||||
|
||||
func (v *aliasView) sorterFn(ss []string) {
|
||||
sort.Strings(ss)
|
||||
}
|
||||
|
||||
// Init the view.
|
||||
func (v *aliasView) init(context.Context, string) {
|
||||
v.update(v.hydrate())
|
||||
|
|
@ -70,6 +66,18 @@ func (v *aliasView) getTitle() string {
|
|||
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 {
|
||||
if !v.cmdBuff.empty() {
|
||||
v.cmdBuff.reset()
|
||||
|
|
@ -123,9 +131,9 @@ func (v *aliasView) hydrate() resource.TableData {
|
|||
cmds := helpCmds()
|
||||
|
||||
data := resource.TableData{
|
||||
Header: resource.Row{"ALIAS", "RESOURCE", "APIGROUP"},
|
||||
Header: resource.Row{"NAME", "RESOURCE", "APIGROUP"},
|
||||
Rows: make(resource.RowEvents, len(cmds)),
|
||||
Namespace: "",
|
||||
Namespace: resource.NotNamespaced,
|
||||
}
|
||||
|
||||
for k := range cmds {
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ func (a *appView) Init(v string, rate int, flags *genericclioptions.ConfigFlags)
|
|||
header := tview.NewFlex()
|
||||
{
|
||||
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(logoView(), 26, 1, false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,12 +33,12 @@ func (v *clusterInfoView) init() {
|
|||
v.SetCell(row, 1, v.infoCell(cluster.UserName()))
|
||||
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))
|
||||
row++
|
||||
|
||||
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))
|
||||
row++
|
||||
|
||||
|
|
|
|||
|
|
@ -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.extraActionsFn = v.extraActions
|
||||
v.getTV().cleanseFn = v.cleanser
|
||||
v.switchPage("ctx")
|
||||
}
|
||||
return &v
|
||||
|
|
@ -39,15 +40,19 @@ func (v *contextView) useCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (v *contextView) useContext(name string) error {
|
||||
ctx := strings.TrimSpace(name)
|
||||
if strings.HasSuffix(ctx, "*") {
|
||||
ctx = strings.TrimRight(ctx, "*")
|
||||
func (*contextView) cleanser(s string) string {
|
||||
name := strings.TrimSpace(s)
|
||||
if strings.HasSuffix(name, "*") {
|
||||
name = strings.TrimRight(name, "*")
|
||||
}
|
||||
if strings.HasSuffix(ctx, "(𝜟)") {
|
||||
ctx = strings.TrimRight(ctx, "(𝜟)")
|
||||
if strings.HasSuffix(name, "(𝜟)") {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
|
@ -59,5 +64,6 @@ func (v *contextView) useContext(name string) error {
|
|||
if tv, ok := v.GetPrimitive("ctx").(*tableView); ok {
|
||||
tv.Select(0, 0)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,8 +88,7 @@ func (v *helpView) init(_ context.Context, _ string) {
|
|||
{"k", "Up"},
|
||||
{"j", "Down"},
|
||||
}
|
||||
fmt.Fprintln(v)
|
||||
fmt.Fprintf(v, "🖲 [aqua::b]%s\n", "View Navigation")
|
||||
fmt.Fprintf(v, "\n🤖 [aqua::b]%s\n", "View Navigation")
|
||||
for _, h := range navigation {
|
||||
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"},
|
||||
{"a", "Aliases view"},
|
||||
}
|
||||
fmt.Fprintln(v)
|
||||
fmt.Fprintf(v, "️️⁉️ [aqua::b]%s\n", "Help")
|
||||
fmt.Fprintf(v, "️️\n😱 [aqua::b]%s\n", "Help")
|
||||
for _, h := range views {
|
||||
fmt.Fprintf(v, "[pink::b]%9s [gray::]%s\n", h.key, h.description)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,15 +128,15 @@ func (v *menuView) buildMenuTable(hh hints) [][]string {
|
|||
}
|
||||
|
||||
var row, col int
|
||||
firstNS, firstCmd := true, true
|
||||
firstCmd := true
|
||||
maxKeys := make([]int, colCount+1)
|
||||
for _, h := range hh {
|
||||
isDigit := menuRX.MatchString(h.mnemonic)
|
||||
if isDigit && firstNS {
|
||||
row, col, firstNS = 0, col+1, false
|
||||
}
|
||||
// if isDigit && firstNS {
|
||||
// row, col, firstNS = 0, 2, false
|
||||
// }
|
||||
if !isDigit && firstCmd {
|
||||
row, col, firstCmd = 0, 0, false
|
||||
row, col, firstCmd = 0, col+1, false
|
||||
}
|
||||
if 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])
|
||||
}
|
||||
}
|
||||
|
||||
return strTable
|
||||
}
|
||||
|
||||
|
|
@ -165,6 +166,7 @@ func (*menuView) toMnemonic(s string) string {
|
|||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
|
||||
return "<" + strings.ToLower(s) + ">"
|
||||
}
|
||||
|
||||
|
|
@ -173,6 +175,7 @@ func (v *menuView) formatMenu(h hint, size int) string {
|
|||
if err == nil {
|
||||
return fmt.Sprintf(menuIndexFmt, i, resource.Truncate(h.description, 14))
|
||||
}
|
||||
|
||||
menuFmt := " [dodgerblue::b]%-" + strconv.Itoa(size+2) + "s [white::d]%s "
|
||||
return fmt.Sprintf(menuFmt, v.toMnemonic(h.mnemonic), h.description)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ func newNamespaceView(t string, app *appView, list resource.List, c colorerFn) r
|
|||
v.extraActionsFn = v.extraActions
|
||||
v.selectedFn = v.getSelectedItem
|
||||
v.decorateDataFn = v.decorate
|
||||
v.getTV().cleanseFn = v.cleanser
|
||||
v.switchPage("ns")
|
||||
return &v
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,6 +143,23 @@ func (v *podView) showLogs(path, co string, previous bool) {
|
|||
func (v *podView) extraActions(aa keyActions) {
|
||||
aa[KeyL] = newKeyAction("Logs", v.logsCmd, 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) {
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ func resourceViews() map[string]resCmd {
|
|||
return map[string]resCmd{
|
||||
"cm": {
|
||||
title: "ConfigMaps",
|
||||
api: "core",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewConfigMapList,
|
||||
colorerFn: defaultColorer,
|
||||
|
|
@ -108,14 +108,14 @@ func resourceViews() map[string]resCmd {
|
|||
},
|
||||
"ctx": {
|
||||
title: "Contexts",
|
||||
api: "core",
|
||||
api: "",
|
||||
viewFn: newContextView,
|
||||
listFn: resource.NewContextList,
|
||||
colorerFn: ctxColorer,
|
||||
},
|
||||
"ds": {
|
||||
title: "DaemonSets",
|
||||
api: "core",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewDaemonSetList,
|
||||
colorerFn: dpColorer,
|
||||
|
|
@ -129,14 +129,14 @@ func resourceViews() map[string]resCmd {
|
|||
},
|
||||
"ep": {
|
||||
title: "EndPoints",
|
||||
api: "core",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewEndpointsList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"ev": {
|
||||
title: "Events",
|
||||
api: "core",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewEventList,
|
||||
colorerFn: evColorer,
|
||||
|
|
@ -164,35 +164,35 @@ func resourceViews() map[string]resCmd {
|
|||
},
|
||||
"no": {
|
||||
title: "Nodes",
|
||||
api: "core",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewNodeList,
|
||||
colorerFn: nsColorer,
|
||||
},
|
||||
"ns": {
|
||||
title: "Namespaces",
|
||||
api: "core",
|
||||
api: "",
|
||||
viewFn: newNamespaceView,
|
||||
listFn: resource.NewNamespaceList,
|
||||
colorerFn: nsColorer,
|
||||
},
|
||||
"po": {
|
||||
title: "Pods",
|
||||
api: "core",
|
||||
api: "",
|
||||
viewFn: newPodView,
|
||||
listFn: resource.NewPodList,
|
||||
colorerFn: podColorer,
|
||||
},
|
||||
"pv": {
|
||||
title: "PersistentVolumes",
|
||||
api: "core",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewPVList,
|
||||
colorerFn: pvColorer,
|
||||
},
|
||||
"pvc": {
|
||||
title: "PersistentVolumeClaims",
|
||||
api: "core",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewPVCList,
|
||||
colorerFn: pvcColorer,
|
||||
|
|
@ -206,7 +206,7 @@ func resourceViews() map[string]resCmd {
|
|||
},
|
||||
"rc": {
|
||||
title: "ReplicationControllers",
|
||||
api: "v1",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewReplicationControllerList,
|
||||
colorerFn: rsColorer,
|
||||
|
|
@ -227,14 +227,14 @@ func resourceViews() map[string]resCmd {
|
|||
},
|
||||
"sa": {
|
||||
title: "ServiceAccounts",
|
||||
api: "core",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewServiceAccountList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"sec": {
|
||||
title: "Secrets",
|
||||
api: "core",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewSecretList,
|
||||
colorerFn: defaultColorer,
|
||||
|
|
@ -248,7 +248,7 @@ func resourceViews() map[string]resCmd {
|
|||
},
|
||||
"svc": {
|
||||
title: "Services",
|
||||
api: "core",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewServiceList,
|
||||
colorerFn: defaultColorer,
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ func newResourceView(title string, app *appView, list resource.List, c colorerFn
|
|||
Pages: tview.NewPages(),
|
||||
}
|
||||
|
||||
tv := newTableView(app, v.title, list.SortFn())
|
||||
tv := newTableView(app, v.title)
|
||||
{
|
||||
tv.SetColorer(c)
|
||||
tv.SetSelectionChangedFunc(v.selChanged)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package views
|
|||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
|
|
@ -19,6 +20,15 @@ const (
|
|||
)
|
||||
|
||||
type (
|
||||
sortFn func(rows resource.Rows, sortCol sortColumn)
|
||||
cleanseFn func(string) string
|
||||
|
||||
sortColumn struct {
|
||||
index int
|
||||
colCount int
|
||||
asc bool
|
||||
}
|
||||
|
||||
tableView struct {
|
||||
*tview.Table
|
||||
|
||||
|
|
@ -28,18 +38,20 @@ type (
|
|||
refreshMX sync.Mutex
|
||||
actions keyActions
|
||||
colorerFn colorerFn
|
||||
sortFn resource.SortFn
|
||||
sortFn sortFn
|
||||
cleanseFn cleanseFn
|
||||
data resource.TableData
|
||||
cmdBuff *cmdBuff
|
||||
sortBuff *cmdBuff
|
||||
tableMX sync.Mutex
|
||||
sortCol sortColumn
|
||||
}
|
||||
)
|
||||
|
||||
func newTableView(app *appView, title string, sortFn resource.SortFn) *tableView {
|
||||
v := tableView{app: app, Table: tview.NewTable()}
|
||||
func newTableView(app *appView, title string) *tableView {
|
||||
v := tableView{app: app, Table: tview.NewTable(), sortCol: sortColumn{0, 0, true}}
|
||||
{
|
||||
v.baseTitle = title
|
||||
v.sortFn = sortFn
|
||||
v.actions = make(keyActions)
|
||||
v.SetBorder(true)
|
||||
v.SetBorderColor(tcell.ColorDodgerBlue)
|
||||
|
|
@ -53,9 +65,14 @@ func newTableView(app *appView, title string, sortFn resource.SortFn) *tableView
|
|||
v.SetInputCapture(v.keyboard)
|
||||
}
|
||||
|
||||
v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd, false)
|
||||
v.actions[tcell.KeyEnter] = newKeyAction("Search", v.filterCmd, false)
|
||||
v.actions[tcell.KeyEscape] = newKeyAction("Reset Filter", v.resetCmd, false)
|
||||
v.actions[KeyShiftI] = newKeyAction("Invert", v.sortInvertCmd, true)
|
||||
v.actions[KeyShiftN] = newKeyAction("Sort Name", v.sortColCmd(0), true)
|
||||
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[KeyG] = newKeyAction("Top", 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
|
||||
}
|
||||
|
||||
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 {
|
||||
if v.app.cmdView.inCmdMode() {
|
||||
return evt
|
||||
}
|
||||
|
||||
v.app.flash(flashInfo, "Filtering...")
|
||||
log.Info().Msg("Entering filtering mode...")
|
||||
v.cmdBuff.reset()
|
||||
v.cmdBuff.setActive(true)
|
||||
return nil
|
||||
|
|
@ -217,49 +266,151 @@ func (v *tableView) filtered() resource.TableData {
|
|||
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) {
|
||||
v.Clear()
|
||||
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
|
||||
for col, h := range data.Header {
|
||||
c := tview.NewTableCell(h)
|
||||
{
|
||||
c.SetExpansion(3)
|
||||
if len(h) == 0 {
|
||||
c.SetExpansion(1)
|
||||
}
|
||||
c.SetTextColor(tcell.ColorWhite)
|
||||
}
|
||||
v.SetCell(row, col, c)
|
||||
v.addHeaderCell(col, h)
|
||||
}
|
||||
row++
|
||||
|
||||
keys := make([]string, 0, len(data.Rows))
|
||||
for k := range data.Rows {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
if v.sortFn != nil {
|
||||
v.sortFn(keys)
|
||||
}
|
||||
keys := v.sortRows(data)
|
||||
groupKeys := map[string][]string{}
|
||||
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
|
||||
if v.colorerFn != nil {
|
||||
fgColor = v.colorerFn(data.Namespace, data.Rows[k])
|
||||
}
|
||||
for col, f := range data.Rows[k].Fields {
|
||||
c := tview.NewTableCell(deltas(data.Rows[k].Deltas[col], f))
|
||||
{
|
||||
c.SetExpansion(3)
|
||||
if len(data.Header[col]) == 0 {
|
||||
c.SetExpansion(1)
|
||||
}
|
||||
c.SetTextColor(fgColor)
|
||||
}
|
||||
v.SetCell(row, col, c)
|
||||
for col, field := range data.Rows[k].Fields {
|
||||
v.addBodyCell(row, col, field, data.Rows[k].Deltas[col], fgColor)
|
||||
}
|
||||
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() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue