derailed 2020-10-30 12:52:15 -06:00
parent 77ffacc2e4
commit 5c1ff0ed7b
21 changed files with 260 additions and 121 deletions

View File

@ -3,7 +3,7 @@ PACKAGE := github.com/derailed/$(NAME)
GIT := $(shell git rev-parse --short HEAD) GIT := $(shell git rev-parse --short HEAD)
SOURCE_DATE_EPOCH ?= $(shell date +%s) SOURCE_DATE_EPOCH ?= $(shell date +%s)
DATE := $(shell date -u -d @${SOURCE_DATE_EPOCH} +%FT%T%Z) DATE := $(shell date -u -d @${SOURCE_DATE_EPOCH} +%FT%T%Z)
VERSION ?= v0.23.1 VERSION ?= v0.23.2
IMG_NAME := derailed/k9s IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION} IMAGE := ${IMG_NAME}:${VERSION}

View File

@ -0,0 +1,35 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# Release v0.23.2
## Notes
Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!
If you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorhip program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)
On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)
---
### Write Mode
K9s is writable by default, meaning you can interact with your cluster and make changes using one shot commands ie edit, delete, scale, etc... There `readOnly` config option that can be specified in the configuration or via a cli arg to override this behavior. In this drop, we're introducing a symmetrical command line arg aka `--write` that overrides a K9s session and make it writable tho the readOnly config option is set to true.
## Inverse Log Filtering
In the last drop, we've introduces reverse filters to filter out resources from table views. Now you will be able to apply inverse filtering on your log views as well via `/!fred`
---
## Resolved Issues/Features
* [Issue #906](https://github.com/derailed/k9s/issues/906) Print resources in pod view. With Feelings. Thanks Claudio!
* [Issue #889](https://github.com/derailed/k9s/issues/889) Disable readOnly config
* [Issue #564](https://github.com/derailed/k9s/issues/564) Invert filter mode on logs
## Resolved PRs
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -28,7 +28,6 @@ var (
version, commit, date = "dev", "dev", client.NA version, commit, date = "dev", "dev", client.NA
k9sFlags *config.Flags k9sFlags *config.Flags
k8sFlags *genericclioptions.ConfigFlags k8sFlags *genericclioptions.ConfigFlags
demoMode = new(bool)
rootCmd = &cobra.Command{ rootCmd = &cobra.Command{
Use: appName, Use: appName,
@ -40,7 +39,6 @@ var (
func init() { func init() {
rootCmd.AddCommand(versionCmd(), infoCmd()) rootCmd.AddCommand(versionCmd(), infoCmd())
initTransientFlags()
initK9sFlags() initK9sFlags()
initK8sFlags() initK8sFlags()
@ -105,28 +103,15 @@ func loadConfiguration() *config.Config {
log.Warn().Msg("Unable to locate K9s config. Generating new configuration...") log.Warn().Msg("Unable to locate K9s config. Generating new configuration...")
} }
if demoMode != nil {
k9sCfg.SetDemoMode(*demoMode)
}
if *k9sFlags.RefreshRate != config.DefaultRefreshRate { if *k9sFlags.RefreshRate != config.DefaultRefreshRate {
k9sCfg.K9s.OverrideRefreshRate(*k9sFlags.RefreshRate) k9sCfg.K9s.OverrideRefreshRate(*k9sFlags.RefreshRate)
} }
if k9sFlags.Headless != nil {
k9sCfg.K9s.OverrideHeadless(*k9sFlags.Headless) k9sCfg.K9s.OverrideHeadless(*k9sFlags.Headless)
}
if k9sFlags.Crumbsless != nil {
k9sCfg.K9s.OverrideCrumbsless(*k9sFlags.Crumbsless) k9sCfg.K9s.OverrideCrumbsless(*k9sFlags.Crumbsless)
}
if k9sFlags.ReadOnly != nil {
k9sCfg.K9s.OverrideReadOnly(*k9sFlags.ReadOnly) k9sCfg.K9s.OverrideReadOnly(*k9sFlags.ReadOnly)
} k9sCfg.K9s.OverrideWrite(*k9sFlags.Write)
if k9sFlags.Command != nil {
k9sCfg.K9s.OverrideCommand(*k9sFlags.Command) k9sCfg.K9s.OverrideCommand(*k9sFlags.Command)
}
if isBoolSet(k9sFlags.AllNamespaces) && k9sCfg.SetActiveNamespace(client.AllNamespaces) != nil { if isBoolSet(k9sFlags.AllNamespaces) && k9sCfg.SetActiveNamespace(client.AllNamespaces) != nil {
log.Error().Msg("Setting active namespace") log.Error().Msg("Setting active namespace")
@ -175,15 +160,6 @@ func parseLevel(level string) zerolog.Level {
} }
} }
func initTransientFlags() {
rootCmd.Flags().BoolVar(
demoMode,
"demo",
false,
"Enable demo mode to show keyboard commands",
)
}
func initK9sFlags() { func initK9sFlags() {
k9sFlags = config.NewFlags() k9sFlags = config.NewFlags()
rootCmd.Flags().IntVarP( rootCmd.Flags().IntVarP(
@ -204,6 +180,12 @@ func initK9sFlags() {
false, false,
"Turn K9s header off", "Turn K9s header off",
) )
rootCmd.Flags().BoolVar(
k9sFlags.Crumbsless,
"crumbsless",
false,
"Turn K9s crumbs off",
)
rootCmd.Flags().BoolVarP( rootCmd.Flags().BoolVarP(
k9sFlags.AllNamespaces, k9sFlags.AllNamespaces,
"all-namespaces", "A", "all-namespaces", "A",
@ -220,13 +202,13 @@ func initK9sFlags() {
k9sFlags.ReadOnly, k9sFlags.ReadOnly,
"readonly", "readonly",
false, false,
"Toggles readOnly mode by overriding configuration setting", "Sets readOnly mode by overriding readOnly configuration setting",
) )
rootCmd.Flags().BoolVar( rootCmd.Flags().BoolVar(
k9sFlags.Crumbsless, k9sFlags.Write,
"crumbsless", "write",
false, false,
"Turn K9s crumbs off", "Sets write mode by overriding the readOnly configuration setting",
) )
} }

View File

@ -52,7 +52,6 @@ type (
K9s *K9s `yaml:"k9s"` K9s *K9s `yaml:"k9s"`
client client.Connection client client.Connection
settings KubeSettings settings KubeSettings
demoMode bool
} }
) )
@ -70,16 +69,6 @@ func NewConfig(ks KubeSettings) *Config {
return &Config{K9s: NewK9s(), settings: ks} return &Config{K9s: NewK9s(), settings: ks}
} }
// DemoMode returns true if demo mode is active, false otherwise.
func (c *Config) DemoMode() bool {
return c.demoMode
}
// SetDemoMode sets the demo mode.
func (c *Config) SetDemoMode(b bool) {
c.demoMode = b
}
// Refine the configuration based on cli args. // Refine the configuration based on cli args.
func (c *Config) Refine(flags *genericclioptions.ConfigFlags) error { func (c *Config) Refine(flags *genericclioptions.ConfigFlags) error {
cfg, err := flags.ToRawKubeConfigLoader().RawConfig() cfg, err := flags.ToRawKubeConfigLoader().RawConfig()

View File

@ -19,6 +19,7 @@ type Flags struct {
Command *string Command *string
AllNamespaces *bool AllNamespaces *bool
ReadOnly *bool ReadOnly *bool
Write *bool
Crumbsless *bool Crumbsless *bool
} }
@ -31,6 +32,7 @@ func NewFlags() *Flags {
Command: strPtr(DefaultCommand), Command: strPtr(DefaultCommand),
AllNamespaces: boolPtr(false), AllNamespaces: boolPtr(false),
ReadOnly: boolPtr(false), ReadOnly: boolPtr(false),
Write: boolPtr(false),
Crumbsless: boolPtr(false), Crumbsless: boolPtr(false),
} }
} }

View File

@ -1,6 +1,8 @@
package config package config
import "github.com/derailed/k9s/internal/client" import (
"github.com/derailed/k9s/internal/client"
)
const ( const (
defaultRefreshRate = 2 defaultRefreshRate = 2
@ -56,8 +58,18 @@ func (k *K9s) OverrideCrumbsless(b bool) {
// OverrideReadOnly set the readonly mode manually. // OverrideReadOnly set the readonly mode manually.
func (k *K9s) OverrideReadOnly(b bool) { func (k *K9s) OverrideReadOnly(b bool) {
if b {
k.manualReadOnly = &b k.manualReadOnly = &b
} }
}
// OverrideWrite set the write mode manually.
func (k *K9s) OverrideWrite(b bool) {
if b {
flag := !b
k.manualReadOnly = &flag
}
}
// OverrideCommand set the command manually. // OverrideCommand set the command manually.
func (k *K9s) OverrideCommand(cmd string) { func (k *K9s) OverrideCommand(cmd string) {
@ -100,6 +112,7 @@ func (k *K9s) IsReadOnly() bool {
if k.manualReadOnly != nil { if k.manualReadOnly != nil {
readOnly = *k.manualReadOnly readOnly = *k.manualReadOnly
} }
return readOnly return readOnly
} }

View File

@ -8,6 +8,57 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestIsReadOnly(t *testing.T) {
uu := map[string]struct {
config string
read, write bool
readOnly bool
}{
"writable": {
config: "k9s.yml",
},
"writable_read_override": {
config: "k9s.yml",
read: true,
readOnly: true,
},
"writable_write_override": {
config: "k9s.yml",
write: true,
},
"readonly": {
config: "k9s_readonly.yml",
readOnly: true,
},
"readonly_read_override": {
config: "k9s_readonly.yml",
read: true,
readOnly: true,
},
"readonly_write_override": {
config: "k9s_readonly.yml",
write: true,
},
"readonly_both_override": {
config: "k9s_readonly.yml",
read: true,
write: true,
},
}
mk := NewMockKubeSettings()
cfg := config.NewConfig(mk)
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Nil(t, cfg.Load("testdata/"+u.config))
cfg.K9s.OverrideReadOnly(u.read)
cfg.K9s.OverrideWrite(u.write)
assert.Equal(t, u.readOnly, cfg.K9s.IsReadOnly())
})
}
}
func TestK9sValidate(t *testing.T) { func TestK9sValidate(t *testing.T) {
mc := NewMockConnection() mc := NewMockConnection()
m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil)

View File

@ -1,5 +1,6 @@
k9s: k9s:
refreshRate: 2 refreshRate: 2
readOnly: false
logger: logger:
tail: 200 tail: 200
buffer: 2000 buffer: 2000

View File

@ -0,0 +1,31 @@
k9s:
refreshRate: 2
readOnly: true
logger:
tail: 200
buffer: 2000
currentContext: minikube
currentCluster: minikube
clusters:
minikube:
namespace:
active: kube-system
favorites:
- default
- kube-public
- istio-system
- all
- kube-system
view:
active: ctx
fred:
namespace:
active: default
favorites:
- default
- kube-public
- istio-system
- all
- kube-system
view:
active: po

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"errors" "errors"
"math" "math"
"regexp"
"github.com/derailed/tview" "github.com/derailed/tview"
runewidth "github.com/mattn/go-runewidth" runewidth "github.com/mattn/go-runewidth"
@ -13,6 +14,19 @@ import (
"k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/printers"
) )
var (
inverseRx = regexp.MustCompile(`\A\!`)
fuzzyRx = regexp.MustCompile(`\A\-f`)
)
// IsInverseSelector checks if inverse char has been provided.
func IsInverseSelector(s string) bool {
if s == "" {
return false
}
return inverseRx.MatchString(s)
}
// IsFuzzySelector checks if filter is fuzzy or not. // IsFuzzySelector checks if filter is fuzzy or not.
func IsFuzzySelector(s string) bool { func IsFuzzySelector(s string) bool {
if s == "" { if s == "" {

View File

@ -180,8 +180,6 @@ func (l LogItems) Filter(q string, showTime bool) ([]int, [][]int, error) {
return matches, indices, nil return matches, indices, nil
} }
var fuzzyRx = regexp.MustCompile(`\A\-f`)
func (l LogItems) fuzzyFilter(q string, showTime bool) ([]int, [][]int) { func (l LogItems) fuzzyFilter(q string, showTime bool) ([]int, [][]int) {
q = strings.TrimSpace(q) q = strings.TrimSpace(q)
matches, indices := make([]int, 0, len(l)), make([][]int, 0, 10) matches, indices := make([]int, 0, len(l)), make([][]int, 0, 10)
@ -195,13 +193,24 @@ func (l LogItems) fuzzyFilter(q string, showTime bool) ([]int, [][]int) {
} }
func (l LogItems) filterLogs(q string, showTime bool) ([]int, [][]int, error) { func (l LogItems) filterLogs(q string, showTime bool) ([]int, [][]int, error) {
var invert bool
if IsInverseSelector(q) {
invert = true
q = q[1:]
}
rx, err := regexp.Compile(`(?i)` + q) rx, err := regexp.Compile(`(?i)` + q)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
matches, indices := make([]int, 0, len(l)), make([][]int, 0, 10) matches, indices := make([]int, 0, len(l)), make([][]int, 0, 10)
for i, line := range l.Lines(showTime) { for i, line := range l.Lines(showTime) {
if locs := rx.FindIndex(line); locs != nil { locs := rx.FindIndex(line)
if locs != nil && invert {
continue
}
if locs == nil && !invert {
continue
}
matches = append(matches, i) matches = append(matches, i)
ii := make([]int, 0, 10) ii := make([]int, 0, 10)
for i := 0; i < len(locs); i += 2 { for i := 0; i < len(locs); i += 2 {
@ -211,7 +220,6 @@ func (l LogItems) filterLogs(q string, showTime bool) ([]int, [][]int, error) {
} }
indices = append(indices, ii) indices = append(indices, ii)
} }
}
return matches, indices, nil return matches, indices, nil
} }

View File

@ -38,7 +38,6 @@ type Log struct {
filter string filter string
lastSent int lastSent int
flushTimeout time.Duration flushTimeout time.Duration
filtering bool
} }
// NewLog returns a new model. // NewLog returns a new model.
@ -176,28 +175,15 @@ func (l *Log) Filter(q string) {
defer l.mx.Unlock() defer l.mx.Unlock()
if len(q) == 0 { if len(q) == 0 {
l.filter, l.filtering = "", false l.filter = ""
l.fireLogCleared() l.fireLogCleared()
l.fireLogBuffChanged(l.lines) l.fireLogBuffChanged(l.lines)
return return
} }
l.filter = q l.filter = q
// BOZO!! No needed since cmdbuff is now throttled!!
if l.filtering {
return
}
l.filtering = true
go func(l *Log) {
<-time.After(500 * time.Millisecond)
l.fireLogCleared() l.fireLogCleared()
l.fireLogBuffChanged(l.lines) l.fireLogBuffChanged(l.lines)
l.mx.Lock()
{
l.filtering = false
}
l.mx.Unlock()
}(l)
} }
func (l *Log) load() error { func (l *Log) load() error {

View File

@ -50,6 +50,10 @@ func TestLogFilter(t *testing.T) {
q: `pod-line-[1-3]{1}`, q: `pod-line-[1-3]{1}`,
e: 4, e: 4,
}, },
"invert": {
q: `!pod-line-1`,
e: 8,
},
"fuzzy": { "fuzzy": {
q: `-f po-l1`, q: `-f po-l1`,
e: 2, e: 2,
@ -75,13 +79,13 @@ func TestLogFilter(t *testing.T) {
m.Notify() m.Notify()
assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 2, v.clearCalled)
assert.Equal(t, 0, v.errCalled) assert.Equal(t, 0, v.errCalled)
assert.Equal(t, u.e, len(v.data)) assert.Equal(t, u.e, len(v.data))
m.ClearFilter() m.ClearFilter()
assert.Equal(t, 2, v.dataCalled) assert.Equal(t, 2, v.dataCalled)
assert.Equal(t, 2, v.clearCalled) assert.Equal(t, 3, v.clearCalled)
assert.Equal(t, 0, v.errCalled) assert.Equal(t, 0, v.errCalled)
assert.Equal(t, size, len(v.data)) assert.Equal(t, size, len(v.data))
}) })
@ -195,7 +199,7 @@ func TestLogTimedout(t *testing.T) {
} }
m.Notify() m.Notify()
assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 2, v.clearCalled)
assert.Equal(t, 0, v.errCalled) assert.Equal(t, 0, v.errCalled)
const e = "\x1b[38;5;209ml\x1b[0m\x1b[38;5;209mi\x1b[0m\x1b[38;5;209mn\x1b[0m\x1b[38;5;209me\x1b[0m\x1b[38;5;209m1\x1b[0m" const e = "\x1b[38;5;209ml\x1b[0m\x1b[38;5;209mi\x1b[0m\x1b[38;5;209mn\x1b[0m\x1b[38;5;209me\x1b[0m\x1b[38;5;209m1\x1b[0m"
assert.Equal(t, e, string(v.data[0])) assert.Equal(t, e, string(v.data[0]))

View File

@ -3,7 +3,6 @@ package model
import ( import (
"context" "context"
"fmt" "fmt"
"time"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
@ -27,10 +26,6 @@ func NewPulseHealth(f dao.Factory) *PulseHealth {
// List returns a canned collection of resources health. // List returns a canned collection of resources health.
func (h *PulseHealth) List(ctx context.Context, ns string) ([]runtime.Object, error) { func (h *PulseHealth) List(ctx context.Context, ns string) ([]runtime.Object, error) {
defer func(t time.Time) {
log.Debug().Msgf("PulseHealthCheck %v", time.Since(t))
}(time.Now())
gvrs := []string{ gvrs := []string{
"v1/pods", "v1/pods",
"v1/events", "v1/events",

View File

@ -145,6 +145,11 @@ func (h Header) HasAge() bool {
return h.IndexOf(ageCol, true) != -1 return h.IndexOf(ageCol, true) != -1
} }
// IsMetricsCol checks if given column index represents metrics.
func (h Header) IsMetricsCol(col int) bool {
return h[col].MX
}
// IsAgeCol checks if given column index is the age column. // IsAgeCol checks if given column index is the age column.
func (h Header) IsAgeCol(col int) bool { func (h Header) IsAgeCol(col int) bool {
if !h.HasAge() || col >= len(h) { if !h.HasAge() || col >= len(h) {

View File

@ -1,6 +1,7 @@
package render package render
import ( import (
"fmt"
"regexp" "regexp"
"sort" "sort"
"strconv" "strconv"
@ -259,30 +260,26 @@ func mapToIfc(m interface{}) (s string) {
func toMcPerc(v1, v2 *resource.Quantity) string { func toMcPerc(v1, v2 *resource.Quantity) string {
m := v1.MilliValue() m := v1.MilliValue()
return toMc(m) + " (" + return fmt.Sprintf("%s (%d%%)", toMc(m), client.ToPercentage(m, v2.MilliValue()))
strconv.Itoa(client.ToPercentage(m, v2.MilliValue())) + "%)"
} }
func toMiPerc(v1, v2 *resource.Quantity) string { func toMiPerc(v1, v2 *resource.Quantity) string {
m := v1.Value() m := v1.Value()
return toMi(m) + " (" + return fmt.Sprintf("%s (%d%%)", toMi(m), client.ToPercentage(m, v2.Value()))
strconv.Itoa(client.ToPercentage(m, v2.Value())) + "%)"
} }
func toMc(v int64) string { func toMc(v int64) string {
if v == 0 { if v == 0 {
return ZeroValue return ZeroValue
} }
p := message.NewPrinter(language.English) return AsThousands(v)
return p.Sprintf("%d", v)
} }
func toMi(v int64) string { func toMi(v int64) string {
if v == 0 { if v == 0 {
return ZeroValue return ZeroValue
} }
p := message.NewPrinter(language.English) return AsThousands(client.ToMB(v))
return p.Sprintf("%d", client.ToMB(v))
} }
func boolPtrToStr(b *bool) string { func boolPtrToStr(b *bool) string {

View File

@ -4,6 +4,7 @@ import (
"reflect" "reflect"
"sort" "sort"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/fvbommel/sortorder" "github.com/fvbommel/sortorder"
@ -146,8 +147,14 @@ func (rr Rows) Find(id string) (int, bool) {
} }
// Sort rows based on column index and order. // Sort rows based on column index and order.
func (rr Rows) Sort(col int, asc bool) { func (rr Rows) Sort(col int, asc, isNum, isDur bool) {
t := RowSorter{Rows: rr, Index: col, Asc: asc} t := RowSorter{
Rows: rr,
Index: col,
IsNumber: isNum,
IsDuration: isDur,
Asc: asc,
}
sort.Sort(t) sort.Sort(t)
} }
@ -157,6 +164,7 @@ func (rr Rows) Sort(col int, asc bool) {
type RowSorter struct { type RowSorter struct {
Rows Rows Rows Rows
Index int Index int
IsNumber, IsDuration bool
Asc bool Asc bool
} }
@ -169,7 +177,7 @@ func (s RowSorter) Swap(i, j int) {
} }
func (s RowSorter) Less(i, j int) bool { func (s RowSorter) Less(i, j int) bool {
return Less(s.Asc, s.Rows[i].Fields[s.Index], s.Rows[j].Fields[s.Index]) return Less(s.Asc, s.IsNumber, s.IsDuration, s.Rows[i].Fields[s.Index], s.Rows[j].Fields[s.Index])
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -185,8 +193,13 @@ func toAgeDuration(dur string) string {
} }
// Less return true if c1 < c2. // Less return true if c1 < c2.
func Less(asc bool, c1, c2 string) bool { func Less(asc, isNumber, isDuration bool, c1, c2 string) bool {
if isNumber {
c1, c2 = strings.Replace(c1, ",", "", -1), strings.Replace(c2, ",", "", -1)
}
if isDuration {
c1, c2 = toAgeDuration(c1), toAgeDuration(c2) c1, c2 = toAgeDuration(c1), toAgeDuration(c2)
}
b := sortorder.NaturalLess(c1, c2) b := sortorder.NaturalLess(c1, c2)
if asc { if asc {
return b return b

View File

@ -196,12 +196,19 @@ func (r RowEvents) FindIndex(id string) (int, bool) {
} }
// Sort rows based on column index and order. // Sort rows based on column index and order.
func (r RowEvents) Sort(ns string, sortCol int, ageCol bool, asc bool) { func (r RowEvents) Sort(ns string, sortCol int, ageCol, numCol, asc bool) {
if sortCol == -1 { if sortCol == -1 {
return return
} }
t := RowEventSorter{NS: ns, Events: r, Index: sortCol, Asc: asc} t := RowEventSorter{
NS: ns,
Events: r,
Index: sortCol,
Asc: asc,
IsNumber: numCol,
IsDuration: ageCol,
}
sort.Sort(t) sort.Sort(t)
iids, fields := map[string][]string{}, make(StringSet, 0, len(r)) iids, fields := map[string][]string{}, make(StringSet, 0, len(r))
@ -230,6 +237,8 @@ type RowEventSorter struct {
Events RowEvents Events RowEvents
Index int Index int
NS string NS string
IsNumber bool
IsDuration bool
Asc bool Asc bool
} }
@ -243,7 +252,7 @@ func (r RowEventSorter) Swap(i, j int) {
func (r RowEventSorter) Less(i, j int) bool { func (r RowEventSorter) Less(i, j int) bool {
f1, f2 := r.Events[i].Row.Fields, r.Events[j].Row.Fields f1, f2 := r.Events[i].Row.Fields, r.Events[j].Row.Fields
return Less(r.Asc, f1[r.Index], f2[r.Index]) return Less(r.Asc, r.IsNumber, r.IsDuration, f1[r.Index], f2[r.Index])
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View File

@ -411,7 +411,7 @@ func TestRowEventsSort(t *testing.T) {
uu := map[string]struct { uu := map[string]struct {
re render.RowEvents re render.RowEvents
col int col int
age, asc bool age, num, asc bool
e render.RowEvents e render.RowEvents
}{ }{
"age_time": { "age_time": {
@ -483,7 +483,7 @@ func TestRowEventsSort(t *testing.T) {
for k := range uu { for k := range uu {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
u.re.Sort("", u.col, u.age, u.asc) u.re.Sort("", u.col, u.age, u.num, u.asc)
assert.Equal(t, u.e, u.re) assert.Equal(t, u.e, u.re)
}) })
} }

View File

@ -233,7 +233,7 @@ func TestRowsSortText(t *testing.T) {
uu := map[string]struct { uu := map[string]struct {
rows render.Rows rows render.Rows
col int col int
asc bool asc, num bool
e render.Rows e render.Rows
}{ }{
"plainAsc": { "plainAsc": {
@ -266,6 +266,7 @@ func TestRowsSortText(t *testing.T) {
{Fields: []string{"1", "blee"}}, {Fields: []string{"1", "blee"}},
}, },
col: 0, col: 0,
num: true,
asc: true, asc: true,
e: render.Rows{ e: render.Rows{
{Fields: []string{"1", "blee"}}, {Fields: []string{"1", "blee"}},
@ -278,6 +279,7 @@ func TestRowsSortText(t *testing.T) {
{Fields: []string{"1", "blee"}}, {Fields: []string{"1", "blee"}},
}, },
col: 0, col: 0,
num: true,
asc: false, asc: false,
e: render.Rows{ e: render.Rows{
{Fields: []string{"10", "duh"}}, {Fields: []string{"10", "duh"}},
@ -301,7 +303,7 @@ func TestRowsSortText(t *testing.T) {
for k := range uu { for k := range uu {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
u.rows.Sort(u.col, u.asc) u.rows.Sort(u.col, u.asc, u.num, false)
assert.Equal(t, u.e, u.rows) assert.Equal(t, u.e, u.rows)
}) })
} }
@ -342,7 +344,7 @@ func TestRowsSortDuration(t *testing.T) {
for k := range uu { for k := range uu {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
u.rows.Sort(u.col, u.asc) u.rows.Sort(u.col, u.asc, false, true)
assert.Equal(t, u.e, u.rows) assert.Equal(t, u.e, u.rows)
}) })
} }
@ -369,13 +371,13 @@ func TestRowsSortMetrics(t *testing.T) {
}, },
"metricDesc": { "metricDesc": {
rows: render.Rows{ rows: render.Rows{
{Fields: []string{"10m", "100Mi"}}, {Fields: []string{"10000m", "1000Mi"}},
{Fields: []string{"1m", "50Mi"}}, {Fields: []string{"1m", "50Mi"}},
}, },
col: 1, col: 1,
asc: false, asc: false,
e: render.Rows{ e: render.Rows{
{Fields: []string{"10m", "100Mi"}}, {Fields: []string{"10000m", "1000Mi"}},
{Fields: []string{"1m", "50Mi"}}, {Fields: []string{"1m", "50Mi"}},
}, },
}, },
@ -384,7 +386,7 @@ func TestRowsSortMetrics(t *testing.T) {
for k := range uu { for k := range uu {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
u.rows.Sort(u.col, u.asc) u.rows.Sort(u.col, u.asc, true, false)
assert.Equal(t, u.e, u.rows) assert.Equal(t, u.e, u.rows)
}) })
} }

View File

@ -234,10 +234,12 @@ func (t *Table) doUpdate(data render.TableData) {
c.SetTextColor(fg) c.SetTextColor(fg)
col++ col++
} }
colIndex := custData.Header.IndexOf(t.sortCol.name, false)
custData.RowEvents.Sort( custData.RowEvents.Sort(
custData.Namespace, custData.Namespace,
custData.Header.IndexOf(t.sortCol.name, false), colIndex,
t.sortCol.name == "AGE", t.sortCol.name == "AGE",
data.Header.IsMetricsCol(colIndex),
t.sortCol.asc, t.sortCol.asc,
) )