From c4beaf920730b14c15567727f712069b99f386e9 Mon Sep 17 00:00:00 2001 From: derailed Date: Thu, 28 Mar 2019 14:34:02 -0600 Subject: [PATCH] address scroll jitter --- internal/resource/cr.go | 4 +- internal/resource/cr_binding.go | 47 ++++++- internal/resource/cr_binding_test.go | 2 +- internal/resource/cr_test.go | 6 +- internal/resource/cronjob.go | 6 +- internal/resource/cronjob_test.go | 4 +- internal/resource/helpers.go | 23 ---- internal/resource/helpers_test.go | 32 ----- internal/resource/job.go | 6 +- internal/resource/job_test.go | 4 +- internal/resource/pod.go | 11 +- internal/resource/pod_test.go | 2 +- internal/resource/ro.go | 6 +- internal/resource/ro_binding.go | 40 +----- internal/resource/ro_binding_int_test.go | 30 +++-- internal/resource/ro_binding_test.go | 2 +- internal/resource/ro_test.go | 2 +- internal/resource/rs.go | 14 +- internal/resource/rs_test.go | 2 +- internal/resource/svc.go | 6 +- internal/resource/svc_test.go | 6 +- internal/views/alias.go | 6 +- internal/views/app.go | 3 + internal/views/command.go | 18 ++- internal/views/context.go | 4 +- internal/views/cronjob.go | 6 +- internal/views/fu.go | 2 +- internal/views/helpers.go | 4 + internal/views/job.go | 10 +- internal/views/namespace.go | 20 ++- internal/views/no.go | 4 +- internal/views/padding.go | 51 ++++++++ internal/views/padding_test.go | 107 +++++++++++++++ internal/views/pod.go | 4 +- internal/views/rbac.go | 4 +- internal/views/rbac_test.go | 14 +- internal/views/registrar.go | 158 +++++++++++------------ internal/views/resource.go | 31 +++-- internal/views/table.go | 94 +++++++------- 39 files changed, 473 insertions(+), 322 deletions(-) create mode 100644 internal/views/padding.go create mode 100644 internal/views/padding_test.go diff --git a/internal/resource/cr.go b/internal/resource/cr.go index a356e93b..397e589d 100644 --- a/internal/resource/cr.go +++ b/internal/resource/cr.go @@ -72,7 +72,7 @@ func (r *ClusterRole) Fields(ns string) Row { i := r.instance return append(ff, - Pad(i.Name, RBACPad), - Pad(toAge(i.ObjectMeta.CreationTimestamp), AgePad), + i.Name, + toAge(i.ObjectMeta.CreationTimestamp), ) } diff --git a/internal/resource/cr_binding.go b/internal/resource/cr_binding.go index f6ddb96c..236127af 100644 --- a/internal/resource/cr_binding.go +++ b/internal/resource/cr_binding.go @@ -1,6 +1,8 @@ package resource import ( + "strings" + "github.com/derailed/k9s/internal/k8s" "github.com/rs/zerolog/log" v1 "k8s.io/api/rbac/v1" @@ -63,15 +65,54 @@ func (r *ClusterRoleBinding) Marshal(path string) (string, error) { // Header return resource header. func (*ClusterRoleBinding) Header(_ string) Row { - return append(Row{}, "NAME", "AGE") + return append(Row{}, "NAME", "ROLE", "KIND", "SUBJECTS", "AGE") } // Fields retrieves displayable fields. func (r *ClusterRoleBinding) Fields(ns string) Row { ff := make(Row, 0, len(r.Header(ns))) + i := r.instance + kind, ss := renderSubjects(i.Subjects) + return append(ff, - r.instance.Name, - toAge(r.instance.ObjectMeta.CreationTimestamp), + i.Name, + i.RoleRef.Name, + kind, + ss, + toAge(i.ObjectMeta.CreationTimestamp), ) } + +// ---------------------------------------------------------------------------- +// Helpers... + +func renderSubjects(ss []v1.Subject) (kind string, subjects string) { + if len(ss) == 0 { + return NAValue, "" + } + + var tt []string + for _, s := range ss { + kind = toSubjectAlias(s.Kind) + tt = append(tt, s.Name) + } + return kind, strings.Join(tt, ",") +} + +func toSubjectAlias(s string) string { + if len(s) == 0 { + return s + } + + switch s { + case v1.UserKind: + return "USR" + case v1.GroupKind: + return "GRP" + case v1.ServiceAccountKind: + return "SA" + default: + return strings.ToUpper(s) + } +} diff --git a/internal/resource/cr_binding_test.go b/internal/resource/cr_binding_test.go index 4325a6ea..75eb1408 100644 --- a/internal/resource/cr_binding_test.go +++ b/internal/resource/cr_binding_test.go @@ -59,7 +59,7 @@ func TestCRBListData(t *testing.T) { assert.Equal(t, 1, len(td.Rows)) assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) row := td.Rows["fred"] - assert.Equal(t, 2, len(row.Deltas)) + assert.Equal(t, 5, len(row.Deltas)) for _, d := range row.Deltas { assert.Equal(t, "", d) } diff --git a/internal/resource/cr_test.go b/internal/resource/cr_test.go index 45dc00ab..c13f274f 100644 --- a/internal/resource/cr_test.go +++ b/internal/resource/cr_test.go @@ -41,12 +41,12 @@ func TestCRListAccess(t *testing.T) { func TestCRFields(t *testing.T) { r := newClusterRole().Fields("blee") - assert.Equal(t, resource.Pad("fred", resource.RBACPad), r[0]) + assert.Equal(t, "fred", r[0]) } func TestCRFieldsAllNS(t *testing.T) { r := newClusterRole().Fields(resource.AllNamespaces) - assert.Equal(t, resource.Pad("fred", resource.RBACPad), r[0]) + assert.Equal(t, "fred", r[0]) } func TestCRMarshal(t *testing.T) { @@ -84,7 +84,7 @@ func TestCRListData(t *testing.T) { for _, d := range row.Deltas { assert.Equal(t, "", d) } - assert.Equal(t, resource.Row{resource.Pad("fred", resource.RBACPad)}, row.Fields[:1]) + assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) } // Helpers... diff --git a/internal/resource/cronjob.go b/internal/resource/cronjob.go index e42899e9..14e449cb 100644 --- a/internal/resource/cronjob.go +++ b/internal/resource/cronjob.go @@ -102,7 +102,7 @@ func (r *CronJob) Fields(ns string) Row { i := r.instance if ns == AllNamespaces { - ff = append(ff, Pad(i.Namespace, NSPad)) + ff = append(ff, i.Namespace) } lastScheduled := "" @@ -111,11 +111,11 @@ func (r *CronJob) Fields(ns string) Row { } return append(ff, - Pad(i.Name, NamePad), + i.Name, i.Spec.Schedule, boolPtrToStr(i.Spec.Suspend), strconv.Itoa(len(i.Status.Active)), lastScheduled, - Pad(toAge(i.ObjectMeta.CreationTimestamp), AgePad), + toAge(i.ObjectMeta.CreationTimestamp), ) } diff --git a/internal/resource/cronjob_test.go b/internal/resource/cronjob_test.go index 560eebec..51f21d6a 100644 --- a/internal/resource/cronjob_test.go +++ b/internal/resource/cronjob_test.go @@ -39,7 +39,7 @@ func TestCronJobListAccess(t *testing.T) { func TestCronJobFields(t *testing.T) { r := newCronJob().Fields("blee") - assert.Equal(t, resource.Pad("fred", resource.NamePad), r[0]) + assert.Equal(t, "fred", r[0]) } func TestCronJobMarshal(t *testing.T) { @@ -75,7 +75,7 @@ func TestCronJobListData(t *testing.T) { for _, d := range row.Deltas { assert.Equal(t, "", d) } - assert.Equal(t, resource.Row{resource.Pad("fred", resource.NamePad)}, row.Fields[:1]) + assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) } // Helpers... diff --git a/internal/resource/helpers.go b/internal/resource/helpers.go index 5868c6d0..d11cf7f3 100644 --- a/internal/resource/helpers.go +++ b/internal/resource/helpers.go @@ -36,14 +36,6 @@ const ( NAValue = "" ) -// Columns Padding... -const ( - NSPad = 13 - NamePad = 50 - AgePad = 5 - RBACPad = 80 -) - func asPerc(f float64) string { return fmt.Sprintf("%d%%", int(f)) } @@ -98,21 +90,6 @@ func toAge(timestamp metav1.Time) string { return duration.HumanDuration(time.Since(timestamp.Time)) } -// FixCol set column width to specified size by either truncating or padding. -func FixCol(s string, size int) string { - if len(s) > size { - return Truncate(s, size) - } - return s + strings.Repeat(" ", size-len(s)) -} - -// Pad a string up to the given length. -func Pad(s string, l int) string { - fmat := "%-" + strconv.Itoa(l) + "s" - - return fmt.Sprintf(fmat, s) -} - // Truncate a string to the given l and suffix ellipsis if needed. func Truncate(str string, width int) string { return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis)) diff --git a/internal/resource/helpers_test.go b/internal/resource/helpers_test.go index 422e64da..f9f6a17d 100644 --- a/internal/resource/helpers_test.go +++ b/internal/resource/helpers_test.go @@ -77,22 +77,6 @@ func TestNa(t *testing.T) { } } -func TestPad(t *testing.T) { - uu := []struct { - s string - l int - e string - }{ - {"fred", 10, "fred "}, - {"fred", 6, "fred "}, - {"fred", 4, "fred"}, - } - - for _, u := range uu { - assert.Equal(t, u.e, Pad(u.s, u.l)) - } -} - func TestTruncate(t *testing.T) { uu := []struct { s string @@ -109,22 +93,6 @@ func TestTruncate(t *testing.T) { } } -func TestSizeCol(t *testing.T) { - uu := []struct { - s string - l int - e string - }{ - {"fred", 3, "fr…"}, - {"01234567890", 10, "012345678…"}, - {"fred", 10, "fred "}, - } - - for _, u := range uu { - assert.Equal(t, u.e, FixCol(u.s, u.l)) - } -} - func TestMapToStr(t *testing.T) { uu := []struct { i map[string]string diff --git a/internal/resource/job.go b/internal/resource/job.go index 027d81ac..5354a66c 100644 --- a/internal/resource/job.go +++ b/internal/resource/job.go @@ -132,18 +132,18 @@ func (r *Job) Fields(ns string) Row { i := r.instance if ns == AllNamespaces { - ff = append(ff, Pad(i.Namespace, NSPad)) + ff = append(ff, i.Namespace) } cc, ii := r.toContainers(i.Spec.Template.Spec) return append(ff, - Pad(i.Name, NamePad), + i.Name, r.toCompletion(i.Spec, i.Status), r.toDuration(i.Status), cc, ii, - Pad(toAge(i.ObjectMeta.CreationTimestamp), AgePad), + toAge(i.ObjectMeta.CreationTimestamp), ) } diff --git a/internal/resource/job_test.go b/internal/resource/job_test.go index d437377f..1501bbc3 100644 --- a/internal/resource/job_test.go +++ b/internal/resource/job_test.go @@ -38,7 +38,7 @@ func TestJobListAccess(t *testing.T) { func TestJobFields(t *testing.T) { r := newJob().Fields("blee") - assert.Equal(t, resource.Pad("fred", resource.NamePad), r[0]) + assert.Equal(t, "fred", r[0]) } func TestJobMarshal(t *testing.T) { @@ -74,7 +74,7 @@ func TestJobListData(t *testing.T) { for _, d := range row.Deltas { assert.Equal(t, "", d) } - assert.Equal(t, resource.Row{resource.Pad("fred", resource.NamePad)}, row.Fields[:1]) + assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) } // Helpers... diff --git a/internal/resource/pod.go b/internal/resource/pod.go index 62498e98..d898b126 100644 --- a/internal/resource/pod.go +++ b/internal/resource/pod.go @@ -203,14 +203,14 @@ func (r *Pod) Fields(ns string) Row { i := r.instance if ns == AllNamespaces { - ff = append(ff, FixCol(i.Namespace, NSPad)) + ff = append(ff, i.Namespace) } ss := i.Status.ContainerStatuses cr, _, rc := r.statuses(ss) return append(ff, - FixCol(i.ObjectMeta.Name, NamePad), + i.ObjectMeta.Name, strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)), r.phase(i.Status), strconv.Itoa(rc), @@ -219,7 +219,7 @@ func (r *Pod) Fields(ns string) Row { i.Status.PodIP, i.Spec.NodeName, r.mapQOS(i.Status.QOSClass), - Pad(toAge(i.ObjectMeta.CreationTimestamp), AgePad), + toAge(i.ObjectMeta.CreationTimestamp), ) } @@ -229,13 +229,14 @@ func (r *Pod) Fields(ns string) Row { func (*Pod) mapQOS(class v1.PodQOSClass) string { switch class { case v1.PodQOSGuaranteed: - return "Ga" + return "GA" case v1.PodQOSBurstable: - return "Bu" + return "BU" default: return "BE" } } + func (r *Pod) statuses(ss []v1.ContainerStatus) (cr, ct, rc int) { for _, c := range ss { if c.State.Terminated != nil { diff --git a/internal/resource/pod_test.go b/internal/resource/pod_test.go index 80d01d33..21109463 100644 --- a/internal/resource/pod_test.go +++ b/internal/resource/pod_test.go @@ -41,7 +41,7 @@ func TestPodListAccess(t *testing.T) { func TestPodFields(t *testing.T) { r := newPod().Fields("blee") - assert.Equal(t, resource.FixCol("fred", 50), r[0]) + assert.Equal(t, "fred", r[0]) } func TestPodMarshal(t *testing.T) { diff --git a/internal/resource/ro.go b/internal/resource/ro.go index b47572c3..0d69057b 100644 --- a/internal/resource/ro.go +++ b/internal/resource/ro.go @@ -79,12 +79,12 @@ func (r *Role) Fields(ns string) Row { i := r.instance if ns == AllNamespaces { - ff = append(ff, Pad(i.Namespace, NSPad)) + ff = append(ff, i.Namespace) } return append(ff, - Pad(i.Name, RBACPad), - Pad(toAge(i.ObjectMeta.CreationTimestamp), AgePad), + i.Name, + toAge(i.ObjectMeta.CreationTimestamp), ) } diff --git a/internal/resource/ro_binding.go b/internal/resource/ro_binding.go index 5788f7d6..5b800755 100644 --- a/internal/resource/ro_binding.go +++ b/internal/resource/ro_binding.go @@ -1,8 +1,6 @@ package resource import ( - "strings" - "github.com/derailed/k9s/internal/k8s" "github.com/rs/zerolog/log" v1 "k8s.io/api/rbac/v1" @@ -70,49 +68,25 @@ func (*RoleBinding) Header(ns string) Row { hh = append(hh, "NAMESPACE") } - return append(hh, "NAME", "ROLE", "SUBJECTS", "AGE") + return append(hh, "NAME", "ROLE", "KIND", "SUBJECTS", "AGE") } // Fields retrieves displayable fields. func (r *RoleBinding) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) i := r.instance + + ff := make(Row, 0, len(r.Header(ns))) if ns == AllNamespaces { ff = append(ff, i.Namespace) } + kind, ss := renderSubjects(i.Subjects) + return append(ff, i.Name, i.RoleRef.Name, - r.toSubjects(i.Subjects), + kind, + ss, toAge(i.ObjectMeta.CreationTimestamp), ) } - -// ---------------------------------------------------------------------------- -// Helpers... - -func (r *RoleBinding) toSubjects(ss []v1.Subject) string { - var acc string - for i, s := range ss { - acc += s.Name + "/" + r.toSubjectAlias(s.Kind) - if i < len(ss)-1 { - acc += "," - } - } - - return acc -} - -func (r *RoleBinding) toSubjectAlias(s string) string { - switch s { - case v1.UserKind: - return "USR" - case v1.GroupKind: - return "GRP" - case v1.ServiceAccountKind: - return "SA" - default: - return strings.ToUpper(s) - } -} diff --git a/internal/resource/ro_binding_int_test.go b/internal/resource/ro_binding_int_test.go index 537a946a..d3ef208d 100644 --- a/internal/resource/ro_binding_int_test.go +++ b/internal/resource/ro_binding_int_test.go @@ -8,8 +8,6 @@ import ( ) func TestToSubjectAlias(t *testing.T) { - r := RoleBinding{} - uu := []struct { i string e string @@ -20,38 +18,44 @@ func TestToSubjectAlias(t *testing.T) { {"fred", "FRED"}, } for _, u := range uu { - assert.Equal(t, u.e, r.toSubjectAlias(u.i)) + assert.Equal(t, u.e, toSubjectAlias(u.i)) } } -func TestToSubjects(t *testing.T) { - r := RoleBinding{} - +func TestRenderSubjects(t *testing.T) { uu := []struct { - i []rbacv1.Subject - e string + ss []rbacv1.Subject + ek string + e string }{ { []rbacv1.Subject{ {Name: "blee", Kind: rbacv1.UserKind}, }, - "blee/USR", + "USR", + "blee", + }, + { + []rbacv1.Subject{}, + "", + "", }, } for _, u := range uu { - assert.Equal(t, u.e, r.toSubjects(u.i)) + kind, ss := renderSubjects(u.ss) + assert.Equal(t, u.e, ss) + assert.Equal(t, u.ek, kind) } } func BenchmarkToSubjects(b *testing.B) { - var r RoleBinding ss := []rbacv1.Subject{ {Name: "blee", Kind: rbacv1.UserKind}, } + b.ResetTimer() b.ReportAllocs() - for i := 0; i < b.N; i++ { - r.toSubjects(ss) + renderSubjects(ss) } } diff --git a/internal/resource/ro_binding_test.go b/internal/resource/ro_binding_test.go index cba2306c..d3bb82a9 100644 --- a/internal/resource/ro_binding_test.go +++ b/internal/resource/ro_binding_test.go @@ -51,7 +51,7 @@ func TestRBListData(t *testing.T) { assert.Equal(t, 1, len(td.Rows)) assert.Equal(t, "blee", l.GetNamespace()) row := td.Rows["blee/fred"] - assert.Equal(t, 4, len(row.Deltas)) + assert.Equal(t, 5, len(row.Deltas)) for _, d := range row.Deltas { assert.Equal(t, "", d) } diff --git a/internal/resource/ro_test.go b/internal/resource/ro_test.go index 1932c772..82bef2bd 100644 --- a/internal/resource/ro_test.go +++ b/internal/resource/ro_test.go @@ -54,7 +54,7 @@ func TestRoleListData(t *testing.T) { for _, d := range row.Deltas { assert.Equal(t, "", d) } - assert.Equal(t, resource.Row{resource.Pad("fred", resource.RBACPad)}, row.Fields[:1]) + assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) } // Helpers... diff --git a/internal/resource/rs.go b/internal/resource/rs.go index 4bd87ef7..c096d1d3 100644 --- a/internal/resource/rs.go +++ b/internal/resource/rs.go @@ -75,18 +75,18 @@ func (*ReplicaSet) Header(ns string) Row { // Fields retrieves displayable fields. func (r *ReplicaSet) Fields(ns string) Row { - ff := make(Row, 0, len(r.Header(ns))) - if ns == AllNamespaces { - ff = append(ff, Pad(r.instance.Namespace, NSPad)) - } - i := r.instance + ff := make(Row, 0, len(r.Header(ns))) + if ns == AllNamespaces { + ff = append(ff, i.Namespace) + } + return append(ff, - Pad(i.Name, NamePad), + i.Name, strconv.Itoa(int(*i.Spec.Replicas)), strconv.Itoa(int(i.Status.Replicas)), strconv.Itoa(int(i.Status.ReadyReplicas)), - Pad(toAge(i.ObjectMeta.CreationTimestamp), AgePad), + toAge(i.ObjectMeta.CreationTimestamp), ) } diff --git a/internal/resource/rs_test.go b/internal/resource/rs_test.go index 6462d73d..486c509e 100644 --- a/internal/resource/rs_test.go +++ b/internal/resource/rs_test.go @@ -54,7 +54,7 @@ func TestReplicaSetListData(t *testing.T) { for _, d := range row.Deltas { assert.Equal(t, "", d) } - assert.Equal(t, resource.Row{resource.Pad("fred", resource.NamePad)}, row.Fields[:1]) + assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) } // Helpers... diff --git a/internal/resource/svc.go b/internal/resource/svc.go index 6fc8bb6a..a9ae820a 100644 --- a/internal/resource/svc.go +++ b/internal/resource/svc.go @@ -91,16 +91,16 @@ func (r *Service) Fields(ns string) Row { i := r.instance if ns == AllNamespaces { - ff = append(ff, Pad(i.Namespace, NSPad)) + ff = append(ff, i.Namespace) } return append(ff, - Pad(i.ObjectMeta.Name, NamePad), + i.ObjectMeta.Name, string(i.Spec.Type), i.Spec.ClusterIP, r.toIPs(i.Spec.Type, r.getSvcExtIPS(i)), r.toPorts(i.Spec.Ports), - Pad(toAge(i.ObjectMeta.CreationTimestamp), AgePad), + toAge(i.ObjectMeta.CreationTimestamp), ) } diff --git a/internal/resource/svc_test.go b/internal/resource/svc_test.go index 994f3199..517abccc 100644 --- a/internal/resource/svc_test.go +++ b/internal/resource/svc_test.go @@ -52,8 +52,8 @@ func TestSvcFields(t *testing.T) { { i: newSvc(), e: resource.Row{ - resource.Pad("blee", resource.NSPad), - resource.Pad("fred", resource.NamePad), + "blee", + "fred", "ClusterIP", "1.1.1.1", "2.2.2.2", @@ -102,7 +102,7 @@ func TestSVCListData(t *testing.T) { for _, d := range row.Deltas { assert.Equal(t, "", d) } - assert.Equal(t, resource.Row{resource.Pad("fred", 50)}, row.Fields[:1]) + assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) } // Helpers... diff --git a/internal/views/alias.go b/internal/views/alias.go index aaf26662..0a0288c5 100644 --- a/internal/views/alias.go +++ b/internal/views/alias.go @@ -124,9 +124,9 @@ func (v *aliasView) hydrate() resource.TableData { for k := range cmds { fields := resource.Row{ - resource.Pad(k, 30), - resource.Pad(cmds[k].title, 30), - resource.Pad(cmds[k].api, 30), + pad(k, 30), + pad(cmds[k].title, 30), + pad(cmds[k].api, 30), } data.Rows[k] = &resource.RowEvent{ Action: resource.New, diff --git a/internal/views/app.go b/internal/views/app.go index 54bfefb4..684b8dde 100644 --- a/internal/views/app.go +++ b/internal/views/app.go @@ -31,7 +31,10 @@ type ( resourceViewer interface { igniter + setEnterFn(enterFn) + setColorerFn(colorerFn) + setDecorateFn(decorateFn) } appView struct { diff --git a/internal/views/command.go b/internal/views/command.go index 776f1cea..167b99ec 100644 --- a/internal/views/command.go +++ b/internal/views/command.go @@ -69,10 +69,16 @@ func (c *command) run(cmd string) bool { } else { r = res.listFn(c.app.conn(), resource.DefaultNamespace) } - v = res.viewFn(res.title, c.app, r, res.colorerFn) + v = res.viewFn(res.title, c.app, r) + if res.colorerFn != nil { + v.setColorerFn(res.colorerFn) + } if res.enterFn != nil { v.setEnterFn(res.enterFn) } + if res.decorateFn != nil { + v.setDecorateFn(res.decorateFn) + } const fmat = "Viewing %s in namespace %s..." c.app.flash(flashInfo, fmt.Sprintf(fmat, res.title, c.app.config.ActiveNamespace())) log.Debug().Msgf("Running command %s", cmd) @@ -87,16 +93,16 @@ func (c *command) run(cmd string) bool { return false } - n := res.Plural - if len(n) == 0 { - n = res.Singular + name := res.Plural + if len(name) == 0 { + name = res.Singular } v = newResourceView( res.Kind, c.app, - resource.NewCustomList(c.app.conn(), "", res.Group, res.Version, n), - defaultColorer, + resource.NewCustomList(c.app.conn(), "", res.Group, res.Version, name), ) + v.setColorerFn(defaultColorer) c.exec(cmd, v) return true } diff --git a/internal/views/context.go b/internal/views/context.go index 6183a110..a47a21b2 100644 --- a/internal/views/context.go +++ b/internal/views/context.go @@ -11,8 +11,8 @@ type contextView struct { *resourceView } -func newContextView(t string, app *appView, list resource.List, c colorerFn) resourceViewer { - v := contextView{newResourceView(t, app, list, c).(*resourceView)} +func newContextView(t string, app *appView, list resource.List) resourceViewer { + v := contextView{newResourceView(t, app, list).(*resourceView)} { v.extraActionsFn = v.extraActions v.getTV().cleanseFn = v.cleanser diff --git a/internal/views/cronjob.go b/internal/views/cronjob.go index 1f464cc2..e96ffafe 100644 --- a/internal/views/cronjob.go +++ b/internal/views/cronjob.go @@ -11,12 +11,13 @@ type cronJobView struct { *resourceView } -func newCronJobView(t string, app *appView, list resource.List, c colorerFn) resourceViewer { +func newCronJobView(t string, app *appView, list resource.List) resourceViewer { v := cronJobView{ - resourceView: newResourceView(t, app, list, c).(*resourceView), + resourceView: newResourceView(t, app, list).(*resourceView), } v.extraActionsFn = v.extraActions v.switchPage("cronjob") + return &v } @@ -30,6 +31,7 @@ func (v *cronJobView) trigger(evt *tcell.EventKey) *tcell.EventKey { v.app.flash(flashErr, "Boom!", err.Error()) return evt } + return nil } diff --git a/internal/views/fu.go b/internal/views/fu.go index ee6a838b..0371335a 100644 --- a/internal/views/fu.go +++ b/internal/views/fu.go @@ -302,7 +302,7 @@ func (v *fuView) prepRow(ns, res, grp, binding string, verbs []string) resource. grp = toGroup(grp) } - return v.makeRow(resource.Pad(ns, nsLen), resource.Pad(res, nameLen), resource.Pad(grp, groupLen), binding, asVerbs(verbs...)) + return v.makeRow(ns, res, grp, binding, asVerbs(verbs...)) } func (*fuView) makeRow(ns, res, group, binding string, verbs []string) resource.Row { diff --git a/internal/views/helpers.go b/internal/views/helpers.go index b4be66b5..2421e97e 100644 --- a/internal/views/helpers.go +++ b/internal/views/helpers.go @@ -11,6 +11,10 @@ func toPerc(f float64) string { } func deltas(c, n string) string { + c, n = strings.TrimSpace(c), strings.TrimSpace(n) + + // log.Debug().Msgf("`%s` vs `%s`", c, n) + if c == "n/a" { return n } diff --git a/internal/views/job.go b/internal/views/job.go index be080239..bf148060 100644 --- a/internal/views/job.go +++ b/internal/views/job.go @@ -10,13 +10,14 @@ type jobView struct { *resourceView } -func newJobView(t string, app *appView, list resource.List, c colorerFn) resourceViewer { - v := jobView{newResourceView(t, app, list, c).(*resourceView)} +func newJobView(t string, app *appView, list resource.List) resourceViewer { + v := jobView{newResourceView(t, app, list).(*resourceView)} { v.extraActionsFn = v.extraActions + v.AddPage("logs", newLogsView(&v), true, false) + v.switchPage("job") } - v.AddPage("logs", newLogsView(&v), true, false) - v.switchPage("job") + return &v } @@ -60,6 +61,7 @@ func (v *jobView) logs(evt *tcell.EventKey) *tcell.EventKey { v.switchPage("logs") l.init() + return nil } diff --git a/internal/views/namespace.go b/internal/views/namespace.go index e4429319..51ff022e 100644 --- a/internal/views/namespace.go +++ b/internal/views/namespace.go @@ -23,13 +23,16 @@ type namespaceView struct { *resourceView } -func newNamespaceView(t string, app *appView, list resource.List, c colorerFn) resourceViewer { - v := namespaceView{newResourceView(t, app, list, c).(*resourceView)} - v.extraActionsFn = v.extraActions - v.selectedFn = v.getSelectedItem - v.decorateDataFn = v.decorate - v.getTV().cleanseFn = v.cleanser - v.switchPage("ns") +func newNamespaceView(t string, app *appView, list resource.List) resourceViewer { + v := namespaceView{newResourceView(t, app, list).(*resourceView)} + { + v.extraActionsFn = v.extraActions + v.selectedFn = v.getSelectedItem + v.decorateFn = v.decorate + v.getTV().cleanseFn = v.cleanser + v.switchPage("ns") + } + return &v } @@ -44,6 +47,7 @@ func (v *namespaceView) switchNsCmd(evt *tcell.EventKey) *tcell.EventKey { } v.useNamespace(v.getSelectedItem()) v.app.gotoResource("po", true) + return nil } @@ -52,6 +56,7 @@ func (v *namespaceView) useNsCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } v.useNamespace(v.getSelectedItem()) + return nil } @@ -92,5 +97,6 @@ func (v *namespaceView) decorate(data resource.TableData) resource.TableData { r.Action = resource.Unchanged } } + return data } diff --git a/internal/views/no.go b/internal/views/no.go index 0be37178..4650a731 100644 --- a/internal/views/no.go +++ b/internal/views/no.go @@ -9,8 +9,8 @@ type nodeView struct { *resourceView } -func newNodeView(t string, app *appView, list resource.List, c colorerFn) resourceViewer { - v := nodeView{newResourceView(t, app, list, c).(*resourceView)} +func newNodeView(t string, app *appView, list resource.List) resourceViewer { + v := nodeView{newResourceView(t, app, list).(*resourceView)} { v.extraActionsFn = v.extraActions v.switchPage("no") diff --git a/internal/views/padding.go b/internal/views/padding.go new file mode 100644 index 00000000..3deb740f --- /dev/null +++ b/internal/views/padding.go @@ -0,0 +1,51 @@ +package views + +import ( + "strings" + "unicode" + + "github.com/derailed/k9s/internal/resource" +) + +type maxyPad []int + +func computeMaxColumns(pads maxyPad, sortCol int, table resource.TableData) { + for index, h := range table.Header { + pads[index] = len(h) + if index == sortCol { + pads[index] = len(h) + 2 + } + } + + var row int + for _, rev := range table.Rows { + for index, field := range rev.Fields { + if len(field) > pads[index] && isASCII(field) { + pads[index] = len([]rune(field)) + } + } + row++ + } +} + +func isASCII(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] > unicode.MaxASCII { + return false + } + } + return true +} + +// Pad a string up to the given length or truncates if greater than length. +func pad(s string, width int) string { + if len(s) == width { + return s + } + + if len(s) > width { + return resource.Truncate(s, width) + } + + return s + strings.Repeat(" ", width-len(s)) +} diff --git a/internal/views/padding_test.go b/internal/views/padding_test.go new file mode 100644 index 00000000..76256418 --- /dev/null +++ b/internal/views/padding_test.go @@ -0,0 +1,107 @@ +package views + +import ( + "testing" + + "github.com/derailed/k9s/internal/resource" + "github.com/stretchr/testify/assert" +) + +func TestMaxColumn(t *testing.T) { + uu := []struct { + t resource.TableData + s int + e maxyPad + }{ + { + resource.TableData{ + Header: resource.Row{"A", "B"}, + Rows: resource.RowEvents{ + "r1": &resource.RowEvent{Fields: resource.Row{"hello", "world"}}, + "r2": &resource.RowEvent{Fields: resource.Row{"yo", "mama"}}, + }, + }, + 0, + maxyPad{5, 5}, + }, + { + resource.TableData{ + Header: resource.Row{"A", "B"}, + Rows: resource.RowEvents{ + "r1": &resource.RowEvent{Fields: resource.Row{"hello", "world"}}, + "r2": &resource.RowEvent{Fields: resource.Row{"yo", "mama"}}, + }, + }, + 1, + maxyPad{5, 5}, + }, + { + resource.TableData{ + Header: resource.Row{"A", "B"}, + Rows: resource.RowEvents{ + "r1": &resource.RowEvent{Fields: resource.Row{"Hello World lord of ipsums 😅", "world"}}, + "r2": &resource.RowEvent{Fields: resource.Row{"o", "mama"}}, + }, + }, + 0, + maxyPad{3, 5}, + }, + } + + for _, u := range uu { + pads := make(maxyPad, len(u.t.Header)) + computeMaxColumns(pads, u.s, u.t) + assert.Equal(t, u.e, pads) + } +} + +func TestIsASCII(t *testing.T) { + uu := []struct { + s string + e bool + }{ + {"hello", true}, + {"Yo! 😄", false}, + {"😄", false}, + } + + for _, u := range uu { + assert.Equal(t, u.e, isASCII(u.s)) + } +} + +func TestPad(t *testing.T) { + uu := []struct { + s string + l int + e string + }{ + {"fred", 3, "fr…"}, + {"01234567890", 10, "012345678…"}, + {"fred", 10, "fred "}, + {"fred", 6, "fred "}, + {"fred", 4, "fred"}, + } + + for _, u := range uu { + assert.Equal(t, u.e, pad(u.s, u.l)) + } +} + +func BenchmarkMaxColumn(b *testing.B) { + table := resource.TableData{ + Header: resource.Row{"A", "B"}, + Rows: resource.RowEvents{ + "r1": &resource.RowEvent{Fields: resource.Row{"hello", "world"}}, + "r2": &resource.RowEvent{Fields: resource.Row{"yo", "mama"}}, + }, + } + + pads := make(maxyPad, len(table.Header)) + + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + computeMaxColumns(pads, 0, table) + } +} diff --git a/internal/views/pod.go b/internal/views/pod.go index b3b9aeb5..6a1cd2e1 100644 --- a/internal/views/pod.go +++ b/internal/views/pod.go @@ -20,8 +20,8 @@ type loggable interface { switchPage(n string) } -func newPodView(t string, app *appView, list resource.List, c colorerFn) resourceViewer { - v := podView{newResourceView(t, app, list, c).(*resourceView)} +func newPodView(t string, app *appView, list resource.List) resourceViewer { + v := podView{newResourceView(t, app, list).(*resourceView)} { v.extraActionsFn = v.extraActions } diff --git a/internal/views/rbac.go b/internal/views/rbac.go index 9a658fc9..bc783714 100644 --- a/internal/views/rbac.go +++ b/internal/views/rbac.go @@ -311,7 +311,7 @@ func prepRow(res, grp string, verbs []string) resource.Row { grp = toGroup(grp) } - return makeRow(resource.Pad(res, nameLen), resource.Pad(grp, groupLen), asVerbs(verbs...)) + return makeRow(res, grp, asVerbs(verbs...)) } func makeRow(res, group string, verbs []string) resource.Row { @@ -329,7 +329,7 @@ func asVerbs(verbs ...string) resource.Row { r := make(resource.Row, 0, len(k8sVerbs)+1) for _, v := range k8sVerbs { - r = append(r, resource.Pad(toVerbIcon(hasVerb(verbs, v)), 4)) + r = append(r, toVerbIcon(hasVerb(verbs, v))) } var unknowns []string diff --git a/internal/views/rbac_test.go b/internal/views/rbac_test.go index 7c341cc2..fb25db88 100644 --- a/internal/views/rbac_test.go +++ b/internal/views/rbac_test.go @@ -59,7 +59,7 @@ func TestParseRules(t *testing.T) { {APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"*"}}, }, map[string]resource.Row{ - "*.*": resource.Row{resource.Pad("*.*", 60), resource.Pad("*", 30), ok, ok, ok, ok, ok, ok, ok, ok, ""}, + "*.*": resource.Row{"*.*", "*", ok, ok, ok, ok, ok, ok, ok, ok, ""}, }, }, { @@ -67,7 +67,7 @@ func TestParseRules(t *testing.T) { {APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"get"}}, }, map[string]resource.Row{ - "*.*": resource.Row{resource.Pad("*.*", 60), resource.Pad("*", 30), ok, nok, nok, nok, nok, nok, nok, nok, ""}, + "*.*": resource.Row{"*.*", "*", ok, nok, nok, nok, nok, nok, nok, nok, ""}, }, }, { @@ -75,7 +75,7 @@ func TestParseRules(t *testing.T) { {APIGroups: []string{""}, Resources: []string{"*"}, Verbs: []string{"list"}}, }, map[string]resource.Row{ - "*": resource.Row{resource.Pad("*", 60), resource.Pad("v1", 30), nok, ok, nok, nok, nok, nok, nok, nok, ""}, + "*": resource.Row{"*", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""}, }, }, { @@ -83,8 +83,8 @@ func TestParseRules(t *testing.T) { {APIGroups: []string{""}, Resources: []string{"pods"}, Verbs: []string{"list"}, ResourceNames: []string{"fred"}}, }, map[string]resource.Row{ - "pods": resource.Row{resource.Pad("pods", 60), resource.Pad("v1", 30), nok, ok, nok, nok, nok, nok, nok, nok, ""}, - "pods/fred": resource.Row{resource.Pad("pods/fred", 60), resource.Pad("v1", 30), nok, ok, nok, nok, nok, nok, nok, nok, ""}, + "pods": resource.Row{"pods", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""}, + "pods/fred": resource.Row{"pods/fred", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""}, }, }, { @@ -92,7 +92,7 @@ func TestParseRules(t *testing.T) { {APIGroups: []string{}, Resources: []string{}, Verbs: []string{"get"}, NonResourceURLs: []string{"/fred"}}, }, map[string]resource.Row{ - "/fred": resource.Row{resource.Pad("/fred", 60), resource.Pad("", 30), ok, nok, nok, nok, nok, nok, nok, nok, ""}, + "/fred": resource.Row{"/fred", "", ok, nok, nok, nok, nok, nok, nok, nok, ""}, }, }, { @@ -100,7 +100,7 @@ func TestParseRules(t *testing.T) { {APIGroups: []string{}, Resources: []string{}, Verbs: []string{"get"}, NonResourceURLs: []string{"fred"}}, }, map[string]resource.Row{ - "/fred": resource.Row{resource.Pad("/fred", 60), resource.Pad("", 30), ok, nok, nok, nok, nok, nok, nok, nok, ""}, + "/fred": resource.Row{"/fred", "", ok, nok, nok, nok, nok, nok, nok, nok, ""}, }, }, } diff --git a/internal/views/registrar.go b/internal/views/registrar.go index 83f6f883..4b368e12 100644 --- a/internal/views/registrar.go +++ b/internal/views/registrar.go @@ -7,20 +7,22 @@ import ( ) type ( - viewFn func(ns string, app *appView, list resource.List, colorer colorerFn) resourceViewer - listFn func(c resource.Connection, ns string) resource.List - listMxFn func(c resource.Connection, mx resource.MetricsServer, ns string) resource.List - colorerFn func(ns string, evt *resource.RowEvent) tcell.Color - enterFn func(app *appView, ns, resource, selection string) + viewFn func(ns string, app *appView, list resource.List) resourceViewer + listFn func(c resource.Connection, ns string) resource.List + listMxFn func(c resource.Connection, mx resource.MetricsServer, ns string) resource.List + colorerFn func(ns string, evt *resource.RowEvent) tcell.Color + enterFn func(app *appView, ns, resource, selection string) + decorateFn func(resource.TableData) resource.TableData resCmd struct { - title string - api string - viewFn viewFn - listFn listFn - listMxFn listMxFn - enterFn enterFn - colorerFn colorerFn + title string + api string + viewFn viewFn + listFn listFn + listMxFn listMxFn + enterFn enterFn + colorerFn colorerFn + decorateFn decorateFn } ) @@ -86,40 +88,36 @@ func showRBAC(app *appView, ns, resource, selection string) { func resourceViews() map[string]resCmd { return map[string]resCmd{ "cm": { - title: "ConfigMaps", - api: "", - viewFn: newResourceView, - listFn: resource.NewConfigMapList, - colorerFn: defaultColorer, + title: "ConfigMaps", + api: "", + viewFn: newResourceView, + listFn: resource.NewConfigMapList, }, "cr": { - title: "ClusterRoles", - api: "rbac.authorization.k8s.io", - viewFn: newResourceView, - listFn: resource.NewClusterRoleList, - enterFn: showRBAC, - colorerFn: defaultColorer, + title: "ClusterRoles", + api: "rbac.authorization.k8s.io", + viewFn: newResourceView, + listFn: resource.NewClusterRoleList, + enterFn: showRBAC, }, "crb": { - title: "ClusterRoleBindings", - api: "rbac.authorization.k8s.io", - viewFn: newResourceView, - listFn: resource.NewClusterRoleBindingList, - colorerFn: defaultColorer, + title: "ClusterRoleBindings", + api: "rbac.authorization.k8s.io", + viewFn: newResourceView, + listFn: resource.NewClusterRoleBindingList, + // decorateFn: crbDecorator, }, "crd": { - title: "CustomResourceDefinitions", - api: "apiextensions.k8s.io", - viewFn: newResourceView, - listFn: resource.NewCRDList, - colorerFn: defaultColorer, + title: "CustomResourceDefinitions", + api: "apiextensions.k8s.io", + viewFn: newResourceView, + listFn: resource.NewCRDList, }, "cj": { - title: "CronJobs", - api: "batch", - viewFn: newCronJobView, - listFn: resource.NewCronJobList, - colorerFn: defaultColorer, + title: "CronJobs", + api: "batch", + viewFn: newCronJobView, + listFn: resource.NewCronJobList, }, "ctx": { title: "Contexts", @@ -143,11 +141,10 @@ func resourceViews() map[string]resCmd { colorerFn: dpColorer, }, "ep": { - title: "EndPoints", - api: "", - viewFn: newResourceView, - listFn: resource.NewEndpointsList, - colorerFn: defaultColorer, + title: "EndPoints", + api: "", + viewFn: newResourceView, + listFn: resource.NewEndpointsList, }, "ev": { title: "Events", @@ -157,25 +154,22 @@ func resourceViews() map[string]resCmd { colorerFn: evColorer, }, "hpa": { - title: "HorizontalPodAutoscalers", - api: "autoscaling", - viewFn: newResourceView, - listFn: resource.NewHPAList, - colorerFn: defaultColorer, + title: "HorizontalPodAutoscalers", + api: "autoscaling", + viewFn: newResourceView, + listFn: resource.NewHPAList, }, "ing": { - title: "Ingress", - api: "extensions", - viewFn: newResourceView, - listFn: resource.NewIngressList, - colorerFn: defaultColorer, + title: "Ingress", + api: "extensions", + viewFn: newResourceView, + listFn: resource.NewIngressList, }, "jo": { - title: "Jobs", - api: "batch", - viewFn: newJobView, - listFn: resource.NewJobList, - colorerFn: defaultColorer, + title: "Jobs", + api: "batch", + viewFn: newJobView, + listFn: resource.NewJobList, }, "no": { title: "Nodes", @@ -220,11 +214,10 @@ func resourceViews() map[string]resCmd { colorerFn: pvcColorer, }, "rb": { - title: "RoleBindings", - api: "rbac.authorization.k8s.io", - viewFn: newResourceView, - listFn: resource.NewRoleBindingList, - colorerFn: defaultColorer, + title: "RoleBindings", + api: "rbac.authorization.k8s.io", + viewFn: newResourceView, + listFn: resource.NewRoleBindingList, }, "rc": { title: "ReplicationControllers", @@ -234,12 +227,11 @@ func resourceViews() map[string]resCmd { colorerFn: rsColorer, }, "ro": { - title: "Roles", - api: "rbac.authorization.k8s.io", - viewFn: newResourceView, - listFn: resource.NewRoleList, - enterFn: showRBAC, - colorerFn: defaultColorer, + title: "Roles", + api: "rbac.authorization.k8s.io", + viewFn: newResourceView, + listFn: resource.NewRoleList, + enterFn: showRBAC, }, "rs": { title: "ReplicaSets", @@ -249,18 +241,16 @@ func resourceViews() map[string]resCmd { colorerFn: rsColorer, }, "sa": { - title: "ServiceAccounts", - api: "", - viewFn: newResourceView, - listFn: resource.NewServiceAccountList, - colorerFn: defaultColorer, + title: "ServiceAccounts", + api: "", + viewFn: newResourceView, + listFn: resource.NewServiceAccountList, }, "sec": { - title: "Secrets", - api: "", - viewFn: newResourceView, - listFn: resource.NewSecretList, - colorerFn: defaultColorer, + title: "Secrets", + api: "", + viewFn: newResourceView, + listFn: resource.NewSecretList, }, "sts": { title: "StatefulSets", @@ -270,11 +260,11 @@ func resourceViews() map[string]resCmd { colorerFn: stsColorer, }, "svc": { - title: "Services", - api: "", - viewFn: newResourceView, - listFn: resource.NewServiceList, - colorerFn: defaultColorer, + title: "Services", + api: "", + viewFn: newResourceView, + listFn: resource.NewServiceList, + // decorateFn: svcDecorator, }, } } diff --git a/internal/views/resource.go b/internal/views/resource.go index 30c4befb..df9c0f63 100644 --- a/internal/views/resource.go +++ b/internal/views/resource.go @@ -45,11 +45,12 @@ type ( enterFn enterFn extraActionsFn func(keyActions) selectedFn func() string - decorateDataFn func(resource.TableData) resource.TableData + decorateFn decorateFn + colorerFn colorerFn } ) -func newResourceView(title string, app *appView, list resource.List, c colorerFn) resourceViewer { +func newResourceView(title string, app *appView, list resource.List) resourceViewer { v := resourceView{ app: app, title: title, @@ -60,7 +61,6 @@ func newResourceView(title string, app *appView, list resource.List, c colorerFn tv := newTableView(app, v.title) { - tv.SetColorer(c) tv.SetSelectionChangedFunc(v.selChanged) } v.AddPage(v.list.GetName(), tv, true, true) @@ -80,6 +80,12 @@ func newResourceView(title string, app *appView, list resource.List, c colorerFn func (v *resourceView) init(ctx context.Context, ns string) { v.selectedItem, v.selectedNS = noSelection, ns + colorer := defaultColorer + if v.colorerFn != nil { + colorer = v.colorerFn + } + v.getTV().setColorer(colorer) + go func(ctx context.Context) { for { select { @@ -107,10 +113,6 @@ func (v *resourceView) selChanged(r, c int) { v.getTV().cmdBuff.setActive(false) } -func (v *resourceView) colorFn(f colorerFn) { - v.getTV().SetColorer(f) -} - func (v *resourceView) getSelectedItem() string { if v.selectedFn != nil { return v.selectedFn() @@ -125,10 +127,19 @@ func (v *resourceView) hints() hints { return v.CurrentPage().Item.(hinter).hints() } +func (v *resourceView) setColorerFn(f colorerFn) { + v.colorerFn = f + v.getTV().setColorer(f) +} + func (v *resourceView) setEnterFn(f enterFn) { v.enterFn = f } +func (v *resourceView) setDecorateFn(f decorateFn) { + v.decorateFn = f +} + // ---------------------------------------------------------------------------- // Actions... @@ -275,12 +286,12 @@ func (v *resourceView) refresh() { v.list.SetNamespace(v.selectedNS) } if err := v.list.Reconcile(); err != nil { - log.Warn().Msgf("Reconcile %v", err) + log.Error().Err(err).Msg("Reconciliation failed") v.app.flash(flashErr, err.Error()) } data := v.list.Data() - if v.decorateDataFn != nil { - data = v.decorateDataFn(data) + if v.decorateFn != nil { + data = v.decorateFn(data) } v.getTV().update(data) v.selectItem(v.selectedRow, 0) diff --git a/internal/views/table.go b/internal/views/table.go index 1645b176..966f6b59 100644 --- a/internal/views/table.go +++ b/internal/views/table.go @@ -213,7 +213,7 @@ func (v *tableView) setDeleted() { } // SetColorer sets up table row color management. -func (v *tableView) SetColorer(f colorerFn) { +func (v *tableView) setColorer(f colorerFn) { v.colorerFn = f } @@ -283,7 +283,7 @@ func (v *tableView) filtered() resource.TableData { return filtered } -func (v *tableView) displayCol(index int, name string) string { +func (v *tableView) sortIndicator(index int, name string) string { if v.sortCol.index != index { return name } @@ -292,13 +292,13 @@ func (v *tableView) displayCol(index int, name string) string { if v.sortCol.asc { order = "↑" } - return fmt.Sprintf("%s[green::]%s[::]", name, order) + return fmt.Sprintf("%s [green::]%s[::]", name, order) } func (v *tableView) doUpdate(data resource.TableData) { v.currentNS = data.Namespace if v.currentNS == resource.AllNamespaces || v.currentNS == "*" { - v.actions[KeyShiftS] = newKeyAction("Sort Namespace", v.sortNamespaceCmd, true) + v.actions[KeyShiftP] = newKeyAction("Sort Namespace", v.sortNamespaceCmd, true) } else { delete(v.actions, KeyShiftS) } @@ -317,9 +317,11 @@ func (v *tableView) doUpdate(data resource.TableData) { v.sortCol.index = 0 } + pads := make(maxyPad, len(data.Header)) + computeMaxColumns(pads, v.sortCol.index, data) var row int for col, h := range data.Header { - v.addHeaderCell(col, h) + v.addHeaderCell(col, h, pads) } row++ @@ -327,62 +329,64 @@ func (v *tableView) doUpdate(data resource.TableData) { if v.sortFn != nil { sortFn = v.sortFn } - - keys := make([]string, len(data.Rows)) - v.sortRows(data.Rows, sortFn, v.sortCol, keys) - 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] { + prim, sec := v.sortAllRows(data.Rows, sortFn) + for _, pk := range prim { + for _, sk := range sec[pk] { fgColor := tcell.ColorGray if v.colorerFn != nil { - fgColor = v.colorerFn(data.Namespace, data.Rows[k]) + fgColor = v.colorerFn(data.Namespace, data.Rows[sk]) } - for col, field := range data.Rows[k].Fields { - v.addBodyCell(row, col, field, data.Rows[k].Deltas[col], fgColor) + for col, field := range data.Rows[sk].Fields { + v.addBodyCell(row, col, field, data.Rows[sk].Deltas[col], fgColor, pads) } row++ } } } -func (v *tableView) addHeaderCell(col int, name string) { - c := tview.NewTableCell(v.displayCol(col, name)) +func (v *tableView) sortAllRows(rows resource.RowEvents, sortFn sortFn) (resource.Row, map[string]resource.Row) { + keys := make([]string, len(rows)) + v.sortRows(rows, sortFn, v.sortCol, keys) + + sec := make(map[string]resource.Row, len(rows)) + for _, k := range keys { + grp := rows[k].Fields[v.sortCol.index] + log.Debug().Msg("append") + sec[grp] = append(sec[grp], k) + } + + // Performs secondary to sort by name for each groups. + prim := make(resource.Row, 0, len(sec)) + for k, v := range sec { + sort.Strings(v) + prim = append(prim, k) + } + rs := groupSorter{prim, v.sortCol.asc} + sort.Sort(rs) + + return prim, sec +} + +func (v *tableView) addHeaderCell(col int, name string, pads maxyPad) { + c := tview.NewTableCell(v.sortIndicator(col, name)) { - c.SetExpansion(3) - if len(name) == 0 { - c.SetExpansion(1) - } + 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)) +func (v *tableView) addBodyCell(row, col int, field, delta string, color tcell.Color, pads maxyPad) { + var pField string + if isASCII(field) { + pField = pad(deltas(delta, field), pads[col]) + } else { + pField = deltas(delta, field) + } + + c := tview.NewTableCell(pField) { - c.SetExpansion(3) - if len(v.GetCell(0, col).Text) == 0 { - c.SetExpansion(1) - } + c.SetExpansion(1) c.SetTextColor(color) } v.SetCell(row, col, c)