address scroll jitter

mine
derailed 2019-03-28 14:34:02 -06:00
parent 6001b004d1
commit c4beaf9207
39 changed files with 473 additions and 322 deletions

View File

@ -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),
)
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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...

View File

@ -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 := "<none>"
@ -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),
)
}

View File

@ -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...

View File

@ -36,14 +36,6 @@ const (
NAValue = "<n/a>"
)
// 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))

View File

@ -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

View File

@ -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),
)
}

View File

@ -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...

View File

@ -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 {

View File

@ -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) {

View File

@ -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),
)
}

View File

@ -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)
}
}

View File

@ -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{},
"<n/a>",
"",
},
}
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)
}
}

View File

@ -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)
}

View File

@ -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...

View File

@ -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),
)
}

View File

@ -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...

View File

@ -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),
)
}

View File

@ -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...

View File

@ -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,

View File

@ -31,7 +31,10 @@ type (
resourceViewer interface {
igniter
setEnterFn(enterFn)
setColorerFn(colorerFn)
setDecorateFn(decorateFn)
}
appView struct {

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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")

51
internal/views/padding.go Normal file
View File

@ -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))
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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("<n/a>", 30), ok, nok, nok, nok, nok, nok, nok, nok, ""},
"/fred": resource.Row{"/fred", "<n/a>", 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("<n/a>", 30), ok, nok, nok, nok, nok, nok, nok, nok, ""},
"/fred": resource.Row{"/fred", "<n/a>", ok, nok, nok, nok, nok, nok, nok, nok, ""},
},
},
}

View File

@ -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,
},
}
}

View File

@ -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)

View File

@ -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)