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)