diff --git a/internal/render/header.go b/internal/render/header.go index 91eab1ae..253189cf 100644 --- a/internal/render/header.go +++ b/internal/render/header.go @@ -17,6 +17,7 @@ type HeaderColumn struct { Wide bool MX bool Time bool + Capacity bool } // Clone copies a header. @@ -163,6 +164,15 @@ func (h Header) IsTimeCol(col int) bool { return h[col].Time } +// IsCapacityCol checks if given column index represents a capacity. +func (h Header) IsCapacityCol(col int) bool { + if col < 0 || col >= len(h) { + return false + } + + return h[col].Capacity +} + // ValidColIndex returns the valid col index or -1 if none. func (h Header) ValidColIndex() int { return h.IndexOf("VALID", true) diff --git a/internal/render/helpers.go b/internal/render/helpers.go index 1988633b..422c6d8f 100644 --- a/internal/render/helpers.go +++ b/internal/render/helpers.go @@ -12,6 +12,7 @@ import ( "github.com/rs/zerolog/log" "golang.org/x/text/language" "golang.org/x/text/message" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/duration" ) @@ -57,6 +58,11 @@ func durationToSeconds(duration string) int64 { return n } +func capacityToNumber(capacity string) int64 { + quantity := resource.MustParse(capacity) + return quantity.Value() +} + // AsThousands prints a number with thousand separator. func AsThousands(n int64) string { p := message.NewPrinter(language.English) diff --git a/internal/render/pv.go b/internal/render/pv.go index eecf98a7..630c2e77 100644 --- a/internal/render/pv.go +++ b/internal/render/pv.go @@ -49,7 +49,7 @@ func (p PersistentVolume) ColorerFunc() ColorerFunc { func (PersistentVolume) Header(string) Header { return Header{ HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "CAPACITY"}, + HeaderColumn{Name: "CAPACITY", Capacity: true}, HeaderColumn{Name: "ACCESS MODES"}, HeaderColumn{Name: "RECLAIM POLICY"}, HeaderColumn{Name: "STATUS"}, diff --git a/internal/render/pvc.go b/internal/render/pvc.go index 809ae1c6..f5514d5b 100644 --- a/internal/render/pvc.go +++ b/internal/render/pvc.go @@ -21,7 +21,7 @@ func (PersistentVolumeClaim) Header(ns string) Header { HeaderColumn{Name: "NAME"}, HeaderColumn{Name: "STATUS"}, HeaderColumn{Name: "VOLUME"}, - HeaderColumn{Name: "CAPACITY"}, + HeaderColumn{Name: "CAPACITY", Capacity: true}, HeaderColumn{Name: "ACCESS MODES"}, HeaderColumn{Name: "STORAGECLASS"}, HeaderColumn{Name: "LABELS", Wide: true}, diff --git a/internal/render/row.go b/internal/render/row.go index 5962c24a..1bd5e387 100644 --- a/internal/render/row.go +++ b/internal/render/row.go @@ -145,12 +145,13 @@ func (rr Rows) Find(id string) (int, bool) { } // Sort rows based on column index and order. -func (rr Rows) Sort(col int, asc, isNum, isDur bool) { +func (rr Rows) Sort(col int, asc, isNum, isDur, isCapacity bool) { t := RowSorter{ Rows: rr, Index: col, IsNumber: isNum, IsDuration: isDur, + IsCapacity: isCapacity, Asc: asc, } sort.Sort(t) @@ -160,10 +161,12 @@ func (rr Rows) Sort(col int, asc, isNum, isDur bool) { // RowSorter sorts rows. type RowSorter struct { - Rows Rows - Index int - IsNumber, IsDuration bool - Asc bool + Rows Rows + Index int + IsNumber bool + IsDuration bool + IsCapacity bool + Asc bool } func (s RowSorter) Len() int { @@ -177,7 +180,7 @@ func (s RowSorter) Swap(i, j int) { func (s RowSorter) Less(i, j int) bool { v1, v2 := s.Rows[i].Fields[s.Index], s.Rows[j].Fields[s.Index] id1, id2 := s.Rows[i].ID, s.Rows[j].ID - less := Less(s.IsNumber, s.IsDuration, id1, id2, v1, v2) + less := Less(s.IsNumber, s.IsDuration, s.IsCapacity, id1, id2, v1, v2) if s.Asc { return less } @@ -188,7 +191,7 @@ func (s RowSorter) Less(i, j int) bool { // Helpers... // Less return true if c1 < c2. -func Less(isNumber, isDuration bool, id1, id2, v1, v2 string) bool { +func Less(isNumber, isDuration, isCapacity bool, id1, id2, v1, v2 string) bool { var less bool switch { case isNumber: @@ -197,6 +200,9 @@ func Less(isNumber, isDuration bool, id1, id2, v1, v2 string) bool { case isDuration: d1, d2 := durationToSeconds(v1), durationToSeconds(v2) less = d1 <= d2 + case isCapacity: + c1, c2 := capacityToNumber(v1), capacityToNumber(v2) + less = c1 <= c2 default: less = sortorder.NaturalLess(v1, v2) } diff --git a/internal/render/row_event.go b/internal/render/row_event.go index 795006c4..83c7c440 100644 --- a/internal/render/row_event.go +++ b/internal/render/row_event.go @@ -193,7 +193,7 @@ func (r RowEvents) FindIndex(id string) (int, bool) { } // Sort rows based on column index and order. -func (r RowEvents) Sort(ns string, sortCol int, isDuration, numCol, asc bool) { +func (r RowEvents) Sort(ns string, sortCol int, isDuration, numCol, isCapacity, asc bool) { if sortCol == -1 { return } @@ -205,6 +205,7 @@ func (r RowEvents) Sort(ns string, sortCol int, isDuration, numCol, asc bool) { Asc: asc, IsNumber: numCol, IsDuration: isDuration, + IsCapacity: isCapacity, } sort.Sort(t) } @@ -218,6 +219,7 @@ type RowEventSorter struct { NS string IsNumber bool IsDuration bool + IsCapacity bool Asc bool } @@ -232,7 +234,7 @@ func (r RowEventSorter) Swap(i, j int) { func (r RowEventSorter) Less(i, j int) bool { f1, f2 := r.Events[i].Row.Fields, r.Events[j].Row.Fields id1, id2 := r.Events[i].Row.ID, r.Events[j].Row.ID - less := Less(r.IsNumber, r.IsDuration, id1, id2, f1[r.Index], f2[r.Index]) + less := Less(r.IsNumber, r.IsDuration, r.IsCapacity, id1, id2, f1[r.Index], f2[r.Index]) if r.Asc { return less } diff --git a/internal/render/row_event_test.go b/internal/render/row_event_test.go index 975815e5..c1066730 100644 --- a/internal/render/row_event_test.go +++ b/internal/render/row_event_test.go @@ -413,6 +413,7 @@ func TestRowEventsSort(t *testing.T) { re render.RowEvents col int duration, num, asc bool + capacity bool e render.RowEvents }{ "age_time": { @@ -464,12 +465,33 @@ func TestRowEventsSort(t *testing.T) { {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3"}}}, }, }, + "capacity": { + re: render.RowEvents{ + {Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3", "1Gi"}}}, + {Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3", "1.1G"}}}, + {Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3", "0.5Ti"}}}, + {Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3", "12e6"}}}, + {Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3", "1234"}}}, + {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3", "0.1Ei"}}}, + }, + col: 3, + asc: true, + capacity: true, + e: render.RowEvents{ + {Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3", "1234"}}}, + {Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3", "12e6"}}}, + {Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3", "1Gi"}}}, + {Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3", "1.1G"}}}, + {Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3", "0.5Ti"}}}, + {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3", "0.1Ei"}}}, + }, + }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - u.re.Sort("", u.col, u.duration, u.num, u.asc) + u.re.Sort("", u.col, u.duration, u.num, u.capacity, u.asc) assert.Equal(t, u.e, u.re) }) } diff --git a/internal/render/row_test.go b/internal/render/row_test.go index 185eb700..82e4728f 100644 --- a/internal/render/row_test.go +++ b/internal/render/row_test.go @@ -304,7 +304,7 @@ func TestRowsSortText(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - u.rows.Sort(u.col, u.asc, u.num, false) + u.rows.Sort(u.col, u.asc, u.num, false, false) assert.Equal(t, u.e, u.rows) }) } @@ -369,7 +369,7 @@ func TestRowsSortDuration(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - u.rows.Sort(u.col, u.asc, false, true) + u.rows.Sort(u.col, u.asc, false, true, false) assert.Equal(t, u.e, u.rows) }) } @@ -411,7 +411,49 @@ func TestRowsSortMetrics(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - u.rows.Sort(u.col, u.asc, true, false) + u.rows.Sort(u.col, u.asc, true, false, false) + assert.Equal(t, u.e, u.rows) + }) + } +} + +func TestRowsSortCapacity(t *testing.T) { + uu := map[string]struct { + rows render.Rows + col int + asc bool + e render.Rows + }{ + "capacityAsc": { + rows: render.Rows{ + {Fields: []string{"10Gi", "duh"}}, + {Fields: []string{"10G", "blee"}}, + }, + col: 0, + asc: true, + e: render.Rows{ + {Fields: []string{"10G", "blee"}}, + {Fields: []string{"10Gi", "duh"}}, + }, + }, + "capacityDesc": { + rows: render.Rows{ + {Fields: []string{"10000m", "1000Mi"}}, + {Fields: []string{"1m", "50Mi"}}, + }, + col: 1, + asc: false, + e: render.Rows{ + {Fields: []string{"10000m", "1000Mi"}}, + {Fields: []string{"1m", "50Mi"}}, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + u.rows.Sort(u.col, u.asc, false, false, true) assert.Equal(t, u.e, u.rows) }) } @@ -419,14 +461,17 @@ func TestRowsSortMetrics(t *testing.T) { func TestLess(t *testing.T) { uu := map[string]struct { - isNumber, isDuration bool - id1, id2 string - v1, v2 string - e bool + isNumber bool + isDuration bool + isCapacity bool + id1, id2 string + v1, v2 string + e bool }{ "years": { isNumber: false, isDuration: true, + isCapacity: false, id1: "id1", id2: "id2", v1: "2y263d", @@ -435,17 +480,38 @@ func TestLess(t *testing.T) { "hours": { isNumber: false, isDuration: true, + isCapacity: false, id1: "id1", id2: "id2", v1: "2y263d", v2: "19h", }, + "capacity1": { + isNumber: false, + isDuration: false, + isCapacity: true, + id1: "id1", + id2: "id2", + v1: "1Gi", + v2: "1G", + e: false, + }, + "capacity2": { + isNumber: false, + isDuration: false, + isCapacity: true, + id1: "id1", + id2: "id2", + v1: "1Gi", + v2: "1Ti", + e: true, + }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, render.Less(u.isNumber, u.isDuration, u.id1, u.id2, u.v1, u.v2)) + assert.Equal(t, u.e, render.Less(u.isNumber, u.isDuration, u.isCapacity, u.id1, u.id2, u.v1, u.v2)) }) } } diff --git a/internal/ui/table.go b/internal/ui/table.go index ad0ea4b4..b412bde6 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -247,6 +247,7 @@ func (t *Table) doUpdate(data *render.TableData) { colIndex, custData.Header.IsTimeCol(colIndex), custData.Header.IsMetricsCol(colIndex), + custData.Header.IsCapacityCol(colIndex), t.sortCol.asc, )