From 9138ec06b0e036b0185b754b85bdc9308b7038fa Mon Sep 17 00:00:00 2001 From: derailed Date: Thu, 21 Mar 2019 23:27:48 -0600 Subject: [PATCH] added pdb + fix ev view + misc bugs --- README.md | 2 +- internal/k8s/pdb.go | 42 +++++++++++ internal/resource/evt.go | 10 +-- internal/resource/evt_test.go | 8 +- internal/resource/pdb.go | 129 +++++++++++++++++++++++++++++++++ internal/resource/pdb_test.go | 128 ++++++++++++++++++++++++++++++++ internal/resource/pod.go | 3 +- internal/views/app.go | 1 + internal/views/colorer.go | 16 ++++ internal/views/colorer_test.go | 29 ++++++++ internal/views/details.go | 1 + internal/views/registrar.go | 7 ++ internal/views/sorter_test.go | 67 +++++++++++++++++ internal/views/sorters.go | 76 +++++++++++-------- internal/views/table.go | 2 + 15 files changed, 478 insertions(+), 43 deletions(-) create mode 100644 internal/k8s/pdb.go create mode 100644 internal/resource/pdb.go create mode 100644 internal/resource/pdb_test.go create mode 100644 internal/views/sorter_test.go diff --git a/README.md b/README.md index 512e6754..fd14657b 100644 --- a/README.md +++ b/README.md @@ -272,5 +272,5 @@ to make this project a reality! --- - © 2018 Imhotep Software LLC. + © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/internal/k8s/pdb.go b/internal/k8s/pdb.go new file mode 100644 index 00000000..6507e273 --- /dev/null +++ b/internal/k8s/pdb.go @@ -0,0 +1,42 @@ +package k8s + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// PodDisruptionBudget represents a PodDisruptionBudget Kubernetes resource. +type PodDisruptionBudget struct{} + +// NewPodDisruptionBudget returns a new PodDisruptionBudget. +func NewPodDisruptionBudget() Res { + return &PodDisruptionBudget{} +} + +// Get a pdb. +func (*PodDisruptionBudget) Get(ns, n string) (interface{}, error) { + opts := metav1.GetOptions{} + return conn.dialOrDie().PolicyV1beta1().PodDisruptionBudgets(ns).Get(n, opts) +} + +// List all pdbs in a given namespace. +func (*PodDisruptionBudget) List(ns string) (Collection, error) { + opts := metav1.ListOptions{} + + rr, err := conn.dialOrDie().PolicyV1beta1().PodDisruptionBudgets(ns).List(opts) + if err != nil { + return Collection{}, err + } + + cc := make(Collection, len(rr.Items)) + for i, r := range rr.Items { + cc[i] = r + } + + return cc, nil +} + +// Delete a pdb. +func (*PodDisruptionBudget) Delete(ns, n string) error { + opts := metav1.DeleteOptions{} + return conn.dialOrDie().PolicyV1beta1().PodDisruptionBudgets(ns).Delete(n, &opts) +} diff --git a/internal/resource/evt.go b/internal/resource/evt.go index 798ccef9..59886901 100644 --- a/internal/resource/evt.go +++ b/internal/resource/evt.go @@ -88,26 +88,26 @@ func (r *Event) Delete(path string) error { // Header return resource header. func (*Event) Header(ns string) Row { - ff := Row{""} + var ff Row if ns == AllNamespaces { ff = append(ff, "NAMESPACE") } return append(ff, "NAME", "REASON", "SOURCE", "COUNT", "MESSAGE", "AGE") } +var rx = regexp.MustCompile(`(.+)\.(.+)`) + // Fields returns display fields. func (r *Event) Fields(ns string) Row { ff := make(Row, 0, len(r.Header(ns))) i := r.instance - ff = append(ff, r.toEmoji(i.Type, i.Reason)) if ns == AllNamespaces { ff = append(ff, i.Namespace) } - rx := regexp.MustCompile(`(.+)\.(.+)`) + return append(ff, - // i.Name, - rx.ReplaceAllString(i.Name, `$1`), + i.Name, i.Reason, i.Source.Component, strconv.Itoa(int(i.Count)), diff --git a/internal/resource/evt_test.go b/internal/resource/evt_test.go index 60f4e12b..fbc795bc 100644 --- a/internal/resource/evt_test.go +++ b/internal/resource/evt_test.go @@ -24,12 +24,12 @@ func TestEventListAccess(t *testing.T) { } func TestEventHeader(t *testing.T) { - assert.Equal(t, resource.Row{"", "NAME", "REASON", "SOURCE", "COUNT", "MESSAGE", "AGE"}, newEvent().Header(resource.DefaultNamespace)) + assert.Equal(t, resource.Row{"NAME", "REASON", "SOURCE", "COUNT", "MESSAGE", "AGE"}, newEvent().Header(resource.DefaultNamespace)) } func TestEventFields(t *testing.T) { r := newEvent().Fields("blee") - assert.Equal(t, resource.Row{"😮", "fred", "blah", "", "1"}, r[:5]) + assert.Equal(t, resource.Row{"fred", "blah", "", "1"}, r[:4]) } func TestEventMarshal(t *testing.T) { @@ -64,11 +64,11 @@ func TestEventListData(t *testing.T) { assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) assert.False(t, l.HasXRay()) row := td.Rows["blee/fred"] - assert.Equal(t, 7, len(row.Deltas)) + assert.Equal(t, 6, len(row.Deltas)) for _, d := range row.Deltas { assert.Equal(t, "", d) } - assert.Equal(t, resource.Row{"😮"}, row.Fields[:1]) + assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) } func TestEventListDescribe(t *testing.T) { diff --git a/internal/resource/pdb.go b/internal/resource/pdb.go new file mode 100644 index 00000000..1b2fe94c --- /dev/null +++ b/internal/resource/pdb.go @@ -0,0 +1,129 @@ +package resource + +import ( + "strconv" + + "github.com/derailed/k9s/internal/k8s" + "github.com/rs/zerolog/log" + v1beta1 "k8s.io/api/policy/v1beta1" +) + +// PodDisruptionBudget that can be displayed in a table and interacted with. +type PodDisruptionBudget struct { + *Base + instance *v1beta1.PodDisruptionBudget +} + +// NewPDBList returns a new resource list. +func NewPDBList(ns string) List { + return NewPDBListWithArgs(ns, NewPDB()) +} + +// NewPDBListWithArgs returns a new resource list. +func NewPDBListWithArgs(ns string, res Resource) List { + return newList(ns, "pdb", res, AllVerbsAccess|DescribeAccess) +} + +// NewPDB returns a new PodDisruptionBudget instance. +func NewPDB() *PodDisruptionBudget { + return NewPDBWithArgs(k8s.NewPodDisruptionBudget()) +} + +// NewPDBWithArgs returns a new Pod instance. +func NewPDBWithArgs(r k8s.Res) *PodDisruptionBudget { + p := &PodDisruptionBudget{ + Base: &Base{ + caller: r, + }, + } + p.creator = p + return p +} + +// NewInstance builds a new PodDisruptionBudget instance from a k8s resource. +func (r *PodDisruptionBudget) NewInstance(i interface{}) Columnar { + pdb := NewPDB() + switch i.(type) { + case *v1beta1.PodDisruptionBudget: + pdb.instance = i.(*v1beta1.PodDisruptionBudget) + case v1beta1.PodDisruptionBudget: + ii := i.(v1beta1.PodDisruptionBudget) + pdb.instance = &ii + case *interface{}: + ptr := *i.(*interface{}) + pdbi := ptr.(v1beta1.PodDisruptionBudget) + pdb.instance = &pdbi + default: + log.Fatal().Msgf("Unknown %#v", i) + } + pdb.path = r.namespacedName(pdb.instance.ObjectMeta) + return pdb +} + +// Marshal resource to yaml. +func (r *PodDisruptionBudget) Marshal(path string) (string, error) { + ns, n := namespaced(path) + i, err := r.caller.Get(ns, n) + if err != nil { + return "", err + } + + pdb := i.(*v1beta1.PodDisruptionBudget) + pdb.TypeMeta.APIVersion = "v1beta1" + pdb.TypeMeta.Kind = "PodDisruptionBudget" + return r.marshalObject(pdb) +} + +// Header return resource header. +func (*PodDisruptionBudget) Header(ns string) Row { + hh := Row{} + if ns == AllNamespaces { + hh = append(hh, "NAMESPACE") + } + return append(hh, + "NAME", + "MIN AVAILABLE", + "MAX_ UNAVAILABLE", + "ALLOWED DISRUPTIONS", + "CURRENT", + "DESIRED", + "EXPECTED", + "AGE", + ) +} + +// Fields retrieves displayable fields. +func (r *PodDisruptionBudget) Fields(ns string) Row { + ff := make(Row, 0, len(r.Header(ns))) + i := r.instance + + if ns == AllNamespaces { + ff = append(ff, i.Namespace) + } + + min := NAValue + if i.Spec.MinAvailable != nil { + min = strconv.Itoa(int(i.Spec.MinAvailable.IntVal)) + } + + max := NAValue + if i.Spec.MaxUnavailable != nil { + max = strconv.Itoa(int(i.Spec.MaxUnavailable.IntVal)) + } + + return append(ff, + i.Name, + min, + max, + strconv.Itoa(int(i.Status.PodDisruptionsAllowed)), + strconv.Itoa(int(i.Status.CurrentHealthy)), + strconv.Itoa(int(i.Status.DesiredHealthy)), + strconv.Itoa(int(i.Status.ExpectedPods)), + toAge(i.ObjectMeta.CreationTimestamp), + ) +} + +// ExtFields returns extra info about the resource. +func (r *PodDisruptionBudget) ExtFields() Properties { + return nil +} diff --git a/internal/resource/pdb_test.go b/internal/resource/pdb_test.go new file mode 100644 index 00000000..e7610619 --- /dev/null +++ b/internal/resource/pdb_test.go @@ -0,0 +1,128 @@ +package resource_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/k8s" + "github.com/derailed/k9s/internal/resource" + m "github.com/petergtz/pegomock" + "github.com/stretchr/testify/assert" + v1beta1 "k8s.io/api/policy/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestPDBListAccess(t *testing.T) { + ns := "blee" + l := resource.NewPDBList(resource.AllNamespaces) + l.SetNamespace(ns) + + assert.Equal(t, ns, l.GetNamespace()) + assert.Equal(t, "pdb", l.GetName()) + for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { + assert.True(t, l.Access(a)) + } +} + +func TestPDBHeader(t *testing.T) { + row := resource.Row{ + "NAME", + "MIN AVAILABLE", + "MAX_ UNAVAILABLE", + "ALLOWED DISRUPTIONS", + "CURRENT", + "DESIRED", + "EXPECTED", + "AGE", + } + assert.Equal(t, row, newPDB().Header(resource.DefaultNamespace)) +} + +func TestPDBFields(t *testing.T) { + r := newPDB().Fields("blee") + assert.Equal(t, "fred", r[0]) +} + +func TestPDBMarshal(t *testing.T) { + setup(t) + + ca := NewMockCaller() + m.When(ca.Get("blee", "fred")).ThenReturn(k8sPDB(), nil) + + cm := resource.NewPDBWithArgs(ca) + ma, err := cm.Marshal("blee/fred") + ca.VerifyWasCalledOnce().Get("blee", "fred") + assert.Nil(t, err) + assert.Equal(t, pdbYaml(), ma) +} + +func TestPDBListData(t *testing.T) { + setup(t) + + ca := NewMockCaller() + m.When(ca.List(resource.NotNamespaced)).ThenReturn(k8s.Collection{*k8sPDB()}, nil) + + l := resource.NewPDBListWithArgs("-", resource.NewPDBWithArgs(ca)) + // Make sure we can get deltas! + for i := 0; i < 2; i++ { + err := l.Reconcile() + assert.Nil(t, err) + } + + ca.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced) + td := l.Data() + assert.Equal(t, 1, len(td.Rows)) + assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) + assert.False(t, l.HasXRay()) + row := td.Rows["blee/fred"] + assert.Equal(t, 8, len(row.Deltas)) + for _, d := range row.Deltas { + assert.Equal(t, "", d) + } + assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) +} + +func TestPDBListDescribe(t *testing.T) { + setup(t) + + ca := NewMockCaller() + m.When(ca.Get("blee", "fred")).ThenReturn(k8sPDB(), nil) + l := resource.NewPDBListWithArgs("blee", resource.NewPDBWithArgs(ca)) + props, err := l.Describe("blee/fred") + + ca.VerifyWasCalledOnce().Get("blee", "fred") + assert.Nil(t, err) + assert.Equal(t, 0, len(props)) +} + +// Helpers... + +func k8sPDB() *v1beta1.PodDisruptionBudget { + return &v1beta1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "blee", + Name: "fred", + CreationTimestamp: metav1.Time{Time: testTime()}, + }, + Spec: v1beta1.PodDisruptionBudgetSpec{}, + } +} + +func newPDB() resource.Columnar { + return resource.NewPDB().NewInstance(k8sPDB()) +} + +func pdbYaml() string { + return `apiVersion: v1beta1 +kind: PodDisruptionBudget +metadata: + creationTimestamp: "2018-12-14T17:36:43Z" + name: fred + namespace: blee +spec: {} +status: + currentHealthy: 0 + desiredHealthy: 0 + disruptionsAllowed: 0 + expectedPods: 0 +` +} diff --git a/internal/resource/pod.go b/internal/resource/pod.go index 9218426d..43055831 100644 --- a/internal/resource/pod.go +++ b/internal/resource/pod.go @@ -142,7 +142,8 @@ func (r *Pod) Logs(c chan<- string, ns, n, co string, lines int64, prev bool) (c stream, err := req.Stream() blocked = false if err != nil { - return cancel, fmt.Errorf("Log tail request failed for pod `%s/%s:%s", ns, n, co) + log.Error().Msgf("Tail logs failed `%s/%s:%s -- %v", ns, n, co, err) + return cancel, fmt.Errorf("%v", err) } go func() { diff --git a/internal/views/app.go b/internal/views/app.go index e56b224c..516192d6 100644 --- a/internal/views/app.go +++ b/internal/views/app.go @@ -84,6 +84,7 @@ func NewApp() *appView { v.actions[tcell.KeyEnter] = newKeyAction("Goto", v.gotoCmd, false) v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false) v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false) + v.actions[tcell.KeyDelete] = newKeyAction("Erase", v.eraseCmd, false) v.actions[tcell.KeyTab] = newKeyAction("Focus", v.focusCmd, false) return &v diff --git a/internal/views/colorer.go b/internal/views/colorer.go index f07ea23a..2e14faa4 100644 --- a/internal/views/colorer.go +++ b/internal/views/colorer.go @@ -106,6 +106,22 @@ func pvcColorer(ns string, r *resource.RowEvent) tcell.Color { return c } +func pdbColorer(ns string, r *resource.RowEvent) tcell.Color { + c := defaultColorer(ns, r) + if r.Action == watch.Added || r.Action == watch.Modified { + return c + } + + markCol := 5 + if ns != resource.AllNamespaces { + markCol = 4 + } + if strings.TrimSpace(r.Fields[markCol]) != strings.TrimSpace(r.Fields[markCol+1]) { + return errColor + } + return stdColor +} + func dpColorer(ns string, r *resource.RowEvent) tcell.Color { c := defaultColorer(ns, r) if r.Action == watch.Added || r.Action == watch.Modified { diff --git a/internal/views/colorer_test.go b/internal/views/colorer_test.go index 7c5f88ff..dccdefd4 100644 --- a/internal/views/colorer_test.go +++ b/internal/views/colorer_test.go @@ -160,6 +160,35 @@ func TestDpColorer(t *testing.T) { } } +func TestPdbColorer(t *testing.T) { + var ( + ns = resource.Row{"blee", "fred", "1", "1", "1", "1", "1"} + nonNS = ns[1:] + bustNS = resource.Row{"blee", "fred", "1", "1", "1", "1", "2"} + bustNoNS = bustNS[1:] + ) + + uu := colorerUCs{ + // Add AllNS + {"", &resource.RowEvent{Action: watch.Added, Fields: ns}, addColor}, + // Add NS + {"blee", &resource.RowEvent{Action: watch.Added, Fields: nonNS}, addColor}, + // Mod AllNS + {"", &resource.RowEvent{Action: watch.Modified, Fields: ns}, modColor}, + // Mod NS + {"blee", &resource.RowEvent{Action: watch.Modified, Fields: nonNS}, modColor}, + // Unchanged cool + {"", &resource.RowEvent{Action: resource.Unchanged, Fields: ns}, stdColor}, + // Bust AllNS + {"", &resource.RowEvent{Action: resource.Unchanged, Fields: bustNS}, errColor}, + // Bust NS + {"blee", &resource.RowEvent{Action: resource.Unchanged, Fields: bustNoNS}, errColor}, + } + for _, u := range uu { + assert.Equal(t, u.e, pdbColorer(u.ns, u.r)) + } +} + func TestPVColorer(t *testing.T) { var ( pv = resource.Row{"blee", "1G", "RO", "Duh", "Bound"} diff --git a/internal/views/details.go b/internal/views/details.go index 73564ec1..0f733138 100644 --- a/internal/views/details.go +++ b/internal/views/details.go @@ -57,6 +57,7 @@ func newDetailsView(app *appView, backFn actionHandler) *detailsView { // v.actions[tcell.KeyEnter] = newKeyAction("Search", v.searchCmd) v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false) v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false) + v.actions[tcell.KeyDelete] = newKeyAction("Erase", v.eraseCmd, false) v.actions[tcell.KeyEscape] = newKeyAction("Back", v.backCmd, true) v.actions[tcell.KeyTab] = newKeyAction("Next Match", v.nextCmd, false) v.actions[tcell.KeyBacktab] = newKeyAction("Previous Match", v.prevCmd, false) diff --git a/internal/views/registrar.go b/internal/views/registrar.go index 064a021c..d1b1c661 100644 --- a/internal/views/registrar.go +++ b/internal/views/registrar.go @@ -176,6 +176,13 @@ func resourceViews() map[string]resCmd { listFn: resource.NewNamespaceList, colorerFn: nsColorer, }, + "pdb": { + title: "PodDiscruptionBudgets", + api: "v1.beta1", + viewFn: newResourceView, + listFn: resource.NewPDBList, + colorerFn: pdbColorer, + }, "po": { title: "Pods", api: "", diff --git a/internal/views/sorter_test.go b/internal/views/sorter_test.go new file mode 100644 index 00000000..0a262f6b --- /dev/null +++ b/internal/views/sorter_test.go @@ -0,0 +1,67 @@ +package views + +import ( + "sort" + "testing" + + "github.com/derailed/k9s/internal/resource" + "github.com/stretchr/testify/assert" +) + +func TestGroupSort(t *testing.T) { + uu := []struct { + order bool + rows []string + expect []string + }{ + {true, []string{"200m", "100m"}, []string{"100m", "200m"}}, + {false, []string{"200m", "100m"}, []string{"200m", "100m"}}, + {true, []string{"10", "1"}, []string{"1", "10"}}, + {false, []string{"10", "1"}, []string{"10", "1"}}, + {true, []string{"100Mi", "10Mi"}, []string{"10Mi", "100Mi"}}, + {false, []string{"100Mi", "10Mi"}, []string{"100Mi", "10Mi"}}, + {true, []string{"xyz", "abc"}, []string{"abc", "xyz"}}, + {false, []string{"xyz", "abc"}, []string{"xyz", "abc"}}, + } + + for _, u := range uu { + g := groupSorter{rows: u.rows, asc: u.order} + sort.Sort(g) + assert.Equal(t, u.expect, g.rows) + } +} + +func TestRowSort(t *testing.T) { + uu := []struct { + order bool + rows resource.Rows + expect resource.Rows + }{ + { + true, + resource.Rows{resource.Row{"200m"}, resource.Row{"100m"}}, + resource.Rows{resource.Row{"100m"}, resource.Row{"200m"}}, + }, + { + false, + resource.Rows{resource.Row{"200m"}, resource.Row{"100m"}}, + resource.Rows{resource.Row{"200m"}, resource.Row{"100m"}}, + }, + { + true, + resource.Rows{resource.Row{"200Mi"}, resource.Row{"100Mi"}}, + resource.Rows{resource.Row{"100Mi"}, resource.Row{"200Mi"}}, + }, + { + false, + resource.Rows{resource.Row{"200Mi"}, resource.Row{"100Mi"}}, + resource.Rows{resource.Row{"200Mi"}, resource.Row{"100Mi"}}, + }, + } + + for _, u := range uu { + r := rowSorter{index: 0, rows: u.rows, asc: u.order} + sort.Sort(r) + assert.Equal(t, u.expect, r.rows) + } +} diff --git a/internal/views/sorters.go b/internal/views/sorters.go index cc5f8746..b628b206 100644 --- a/internal/views/sorters.go +++ b/internal/views/sorters.go @@ -17,28 +17,13 @@ type rowSorter struct { 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 + return less(s.asc, s.rows[i][s.index], s.rows[j][s.index]) } // ---------------------------------------------------------------------------- @@ -51,32 +36,59 @@ type groupSorter struct { 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 +func (s groupSorter) Less(i, j int) bool { + return less(s.asc, s.rows[i], s.rows[j]) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func less(asc bool, c1, c2 string) bool { + if o, ok := isMetricSort(asc, c1, c2); ok { + return o + } + + if o, ok := isIntegerSort(asc, c1, c2); ok { + return o } c := strings.Compare(c1, c2) - if s.asc { + if asc { return c < 0 } return c > 0 } -// ---------------------------------------------------------------------------- -// Helpers... +func isMetricSort(asc bool, c1, c2 string) (bool, bool) { + m1, ok := isMetric(c1) + if !ok { + return false, false + } + m2, _ := isMetric(c2) + i1, _ := strconv.Atoi(m1) + i2, _ := strconv.Atoi(m2) + if asc { + return i1 < i2, true + } + return i1 > i2, true +} + +func isIntegerSort(asc bool, c1, c2 string) (bool, bool) { + n1, err := strconv.Atoi(c1) + if err != nil { + return false, false + } + n2, _ := strconv.Atoi(c2) + if asc { + return n1 < n2, true + } + return n1 > n2, true +} var metricRX = regexp.MustCompile(`\A(\d+)(m|Mi)\z`) diff --git a/internal/views/table.go b/internal/views/table.go index af7ff05c..56ddc310 100644 --- a/internal/views/table.go +++ b/internal/views/table.go @@ -74,6 +74,8 @@ func newTableView(app *appView, title string) *tableView { v.actions[tcell.KeyEnter] = newKeyAction("Filter", v.filterCmd, false) v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false) + v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false) + v.actions[tcell.KeyDelete] = newKeyAction("Erase", v.eraseCmd, false) v.actions[KeyG] = newKeyAction("Top", app.puntCmd, false) v.actions[KeyShiftG] = newKeyAction("Bottom", app.puntCmd, false) v.actions[KeyB] = newKeyAction("Down", v.pageDownCmd, false)