added pdb + fix ev view + misc bugs
parent
fde9b0e981
commit
9138ec06b0
|
|
@ -272,5 +272,5 @@ to make this project a reality!
|
|||
|
||||
---
|
||||
|
||||
<img src="assets/imhotep_logo.png" width="32" height="auto"/> © 2018 Imhotep Software LLC.
|
||||
<img src="assets/imhotep_logo.png" width="32" height="auto"/> © 2019 Imhotep Software LLC.
|
||||
All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
`
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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: "",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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`)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue