added support for custom columns
parent
c4305e38ee
commit
42e2e98e4d
2
go.sum
2
go.sum
|
|
@ -841,8 +841,10 @@ gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81
|
|||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
|
||||
helm.sh/helm v2.16.3+incompatible h1:a7P7FSGTBdK6ZsAcWWZZQXPIdzkgybD8CWd/Dy+jwf4=
|
||||
helm.sh/helm/v3 v3.0.2 h1:BggvLisIMrAc+Is5oAHVrlVxgwOOrMN8nddfQbm5gKo=
|
||||
helm.sh/helm/v3 v3.0.2/go.mod h1:KBxE6XWO57XSNA1PA9CvVLYRY0zWqYQTad84bNXp1lw=
|
||||
helm.sh/helm/v3 v3.1.1 h1:aykwPMVyQyncZ8iLNVMXgJ1l3c6W0+LSOPmqp8JdCjs=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ const (
|
|||
cacheSize = 100
|
||||
cacheExpiry = 5 * time.Minute
|
||||
cacheMXKey = "metrics"
|
||||
cacheMXAPIKey = "metricsAPI"
|
||||
checkConnTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
|
|
@ -41,6 +42,15 @@ type APIClient struct {
|
|||
config *Config
|
||||
mx sync.Mutex
|
||||
cache *cache.LRUExpireCache
|
||||
metricsAPI bool
|
||||
}
|
||||
|
||||
// NewTestAPIClient for testing ONLY!!
|
||||
func NewTestClient() *APIClient {
|
||||
return &APIClient{
|
||||
config: NewConfig(nil),
|
||||
cache: cache.NewLRUExpireCache(cacheSize),
|
||||
}
|
||||
}
|
||||
|
||||
// InitConnectionOrDie initialize connection from command line args.
|
||||
|
|
@ -50,8 +60,7 @@ func InitConnectionOrDie(config *Config) *APIClient {
|
|||
config: config,
|
||||
cache: cache.NewLRUExpireCache(cacheSize),
|
||||
}
|
||||
a.HasMetrics()
|
||||
|
||||
a.metricsAPI = a.supportsMetricsResources()
|
||||
return &a
|
||||
}
|
||||
|
||||
|
|
@ -174,6 +183,9 @@ func (a *APIClient) Config() *Config {
|
|||
|
||||
// HasMetrics returns true if the cluster supports metrics.
|
||||
func (a *APIClient) HasMetrics() bool {
|
||||
if !a.supportsMetricsResources() {
|
||||
return false
|
||||
}
|
||||
v, ok := a.cache.Get(cacheMXKey)
|
||||
if ok {
|
||||
flag, k := v.(bool)
|
||||
|
|
@ -186,7 +198,6 @@ func (a *APIClient) HasMetrics() bool {
|
|||
a.cache.Add(cacheMXKey, flag, cacheExpiry)
|
||||
return flag
|
||||
}
|
||||
|
||||
if _, err := dial.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{Limit: 1}); err == nil {
|
||||
flag = true
|
||||
}
|
||||
|
|
@ -282,7 +293,8 @@ func (a *APIClient) SwitchContext(ctx string) error {
|
|||
}
|
||||
a.clearCache()
|
||||
a.reset()
|
||||
_ = a.supportsMxServer()
|
||||
a.metricsAPI = a.supportsMetricsResources()
|
||||
ResetMetrics()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -295,11 +307,20 @@ func (a *APIClient) reset() {
|
|||
a.client, a.dClient, a.nsClient, a.mxsClient = nil, nil, nil, nil
|
||||
}
|
||||
|
||||
func (a *APIClient) supportsMxServer() (supported bool) {
|
||||
func (a *APIClient) supportsMetricsResources() (supported bool) {
|
||||
defer func() {
|
||||
a.cache.Add(cacheMXKey, supported, cacheExpiry)
|
||||
a.cache.Add(cacheMXAPIKey, supported, cacheExpiry)
|
||||
}()
|
||||
|
||||
if v, ok := a.cache.Get(cacheMXAPIKey); ok {
|
||||
flag, k := v.(bool)
|
||||
supported = k && flag
|
||||
return
|
||||
}
|
||||
if a.config == nil || a.config.flags == nil {
|
||||
return
|
||||
}
|
||||
|
||||
apiGroups, err := a.CachedDiscoveryOrDie().ServerGroups()
|
||||
if err != nil {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -25,21 +25,25 @@ const (
|
|||
|
||||
// NotNamespaced designates a non resource namespace.
|
||||
NotNamespaced = "*"
|
||||
)
|
||||
|
||||
const (
|
||||
// CreateVerb represents create access on a resource.
|
||||
CreateVerb = "create"
|
||||
|
||||
// UpdateVerb represents an update access on a resource.
|
||||
UpdateVerb = "update"
|
||||
|
||||
// PatchVerb represents a patch access on a resource.
|
||||
PatchVerb = "patch"
|
||||
|
||||
// DeleteVerb represents a delete access on a resource.
|
||||
DeleteVerb = "delete"
|
||||
|
||||
// GetVerb represents a get access on a resource.
|
||||
GetVerb = "get"
|
||||
|
||||
// ListVerb represents a list access on a resource.
|
||||
ListVerb = "list"
|
||||
|
||||
// WatchVerb represents a watch access on a resource.
|
||||
WatchVerb = "watch"
|
||||
)
|
||||
|
|
@ -65,16 +69,37 @@ type Authorizer interface {
|
|||
type Connection interface {
|
||||
Authorizer
|
||||
|
||||
// Config returns current config.
|
||||
Config() *Config
|
||||
|
||||
// DialOrDie connects to api server.
|
||||
DialOrDie() kubernetes.Interface
|
||||
|
||||
// SwitchContext switches cluster based on context.
|
||||
SwitchContext(ctx string) error
|
||||
|
||||
// CachedDiscoveryOrDie connects to discovery client.
|
||||
CachedDiscoveryOrDie() *disk.CachedDiscoveryClient
|
||||
|
||||
// RestConfigOrDie connects to rest client.
|
||||
RestConfigOrDie() *restclient.Config
|
||||
|
||||
// MXDial connects to metrics server.
|
||||
MXDial() (*versioned.Clientset, error)
|
||||
|
||||
// DynDialOrDie connects to dynamic client.
|
||||
DynDialOrDie() dynamic.Interface
|
||||
|
||||
// HasMetrics checks if metrics server is available.
|
||||
HasMetrics() bool
|
||||
|
||||
// ValidNamespaces returns all available namespaces.
|
||||
ValidNamespaces() ([]v1.Namespace, error)
|
||||
|
||||
// ServerVersion returns current server version.
|
||||
ServerVersion() (*version.Info, error)
|
||||
|
||||
// CheckConnectivity checks if api server connection is happy or not.
|
||||
CheckConnectivity() bool
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@ func (c *Config) Load(path string) error {
|
|||
func (c *Config) Save() error {
|
||||
log.Debug().Msg("[Config] Saving configuration...")
|
||||
c.Validate()
|
||||
|
||||
return c.SaveFile(K9sConfigFile)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,10 +9,8 @@ import (
|
|||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
// K9sStylesFile represents K9s skins file location.
|
||||
K9sStylesFile = filepath.Join(K9sHome, "skin.yml")
|
||||
)
|
||||
// K9sStylesFile represents K9s skins file location.
|
||||
var K9sStylesFile = filepath.Join(K9sHome, "skin.yml")
|
||||
|
||||
// StyleListener represents a skin's listener.
|
||||
type StyleListener interface {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
k9s:
|
||||
views:
|
||||
v1/pods:
|
||||
columns:
|
||||
- NAMESPACE
|
||||
- NAME
|
||||
- AGE
|
||||
- IP
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var K9sViewConfigFile = filepath.Join(K9sHome, "views_config.yml")
|
||||
|
||||
// ViewConfigListener represents a view config listener.
|
||||
type ViewConfigListener interface {
|
||||
// ConfigChanged notifies listener the view configuration changed.
|
||||
ViewSettingsChanged(ViewSetting)
|
||||
}
|
||||
|
||||
type ViewSetting struct {
|
||||
Columns []string `yaml:"columns"`
|
||||
}
|
||||
|
||||
type ViewSettings struct {
|
||||
Fuck string `yaml:"fuck"`
|
||||
Views map[string]ViewSetting `yaml:"views"`
|
||||
}
|
||||
|
||||
func NewViewSettings() ViewSettings {
|
||||
return ViewSettings{
|
||||
Views: make(map[string]ViewSetting),
|
||||
}
|
||||
}
|
||||
|
||||
// CustomView represents a collection of view customization.
|
||||
type CustomView struct {
|
||||
K9s ViewSettings `yaml:"k9s"`
|
||||
listeners map[string]ViewConfigListener
|
||||
}
|
||||
|
||||
func NewCustomView() *CustomView {
|
||||
return &CustomView{
|
||||
K9s: NewViewSettings(),
|
||||
listeners: make(map[string]ViewConfigListener),
|
||||
}
|
||||
}
|
||||
|
||||
// Reset clears out configurations.
|
||||
func (v *CustomView) Reset() {
|
||||
for k := range v.K9s.Views {
|
||||
delete(v.K9s.Views, k)
|
||||
}
|
||||
}
|
||||
|
||||
// Load loads view configurations.
|
||||
func (v *CustomView) Load(path string) error {
|
||||
raw, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var in CustomView
|
||||
if err := yaml.Unmarshal(raw, &in); err != nil {
|
||||
return err
|
||||
}
|
||||
v.K9s = in.K9s
|
||||
v.fireConfigChanged()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddListener registers a new listener.
|
||||
func (c *CustomView) AddListener(gvr string, l ViewConfigListener) {
|
||||
c.listeners[gvr] = l
|
||||
c.fireConfigChanged()
|
||||
}
|
||||
|
||||
// RemoveListener unregister a listener.
|
||||
func (c *CustomView) RemoveListener(gvr string) {
|
||||
delete(c.listeners, gvr)
|
||||
|
||||
}
|
||||
|
||||
func (c *CustomView) fireConfigChanged() {
|
||||
for gvr, list := range c.listeners {
|
||||
if v, ok := c.K9s.Views[gvr]; ok {
|
||||
list.ViewSettingsChanged(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package config_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestViewSettingsLoad(t *testing.T) {
|
||||
cfg := config.NewCustomView()
|
||||
|
||||
assert.Nil(t, cfg.Load("testdata/view_settings.yml"))
|
||||
assert.Equal(t, 1, len(cfg.K9s.Views))
|
||||
assert.Equal(t, 4, len(cfg.K9s.Views["v1/pods"].Columns))
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) {
|
|||
var pmx *mv1beta1.PodMetrics
|
||||
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
|
||||
if pmx, err = client.DialMetrics(p.Client()).FetchPodMetrics(path); err != nil {
|
||||
log.Warn().Err(err).Msgf("No pod metrics")
|
||||
log.Debug().Err(err).Msgf("No pod metrics")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
|||
var pmx *mv1beta1.PodMetricsList
|
||||
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
|
||||
if pmx, err = client.DialMetrics(p.Client()).FetchPodsMetrics(ns); err != nil {
|
||||
log.Warn().Err(err).Msgf("No pods metrics")
|
||||
log.Debug().Err(err).Msgf("No pods metrics")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ func loadRBAC(m ResourceMetas) {
|
|||
func loadPreferred(f Factory, m ResourceMetas) error {
|
||||
rr, err := f.Client().CachedDiscoveryOrDie().ServerPreferredResources()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("Failed to load preferred resources")
|
||||
log.Debug().Err(err).Msgf("Failed to load preferred resources")
|
||||
}
|
||||
for _, r := range rr {
|
||||
for _, res := range r.APIResources {
|
||||
|
|
|
|||
|
|
@ -27,4 +27,5 @@ const (
|
|||
KeyMetrics ContextKey = "metrics"
|
||||
KeyToast ContextKey = "toast"
|
||||
KeyWithMetrics ContextKey = "withMetrics"
|
||||
KeyViewConfig ContextKey = "viewConfig"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ func (h *PulseHealth) check(ctx context.Context, ns, gvr string) (*health.Check,
|
|||
if err := re.Render(o, ns, &rr[i]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !render.Happy(ns, rr[i]) {
|
||||
if !render.Happy(ns, re.Header(ns), rr[i]) {
|
||||
c.Inc(health.Toast)
|
||||
} else {
|
||||
c.Inc(health.OK)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ type TableListener interface {
|
|||
|
||||
// Table represents a table model.
|
||||
type Table struct {
|
||||
gvr string
|
||||
gvr client.GVR
|
||||
namespace string
|
||||
data *render.TableData
|
||||
listeners []TableListener
|
||||
|
|
@ -37,10 +37,11 @@ type Table struct {
|
|||
refreshRate time.Duration
|
||||
instance string
|
||||
mx sync.RWMutex
|
||||
hasMetrics bool
|
||||
}
|
||||
|
||||
// NewTable returns a new table model.
|
||||
func NewTable(gvr string) *Table {
|
||||
func NewTable(gvr client.GVR) *Table {
|
||||
return &Table{
|
||||
gvr: gvr,
|
||||
data: render.NewTableData(),
|
||||
|
|
@ -48,6 +49,11 @@ func NewTable(gvr string) *Table {
|
|||
}
|
||||
}
|
||||
|
||||
// HasMetrics determines if metrics are available on cluster.
|
||||
func (t *Table) HasMetrics() bool {
|
||||
return t.hasMetrics
|
||||
}
|
||||
|
||||
// SetInstance sets a single entry table.
|
||||
func (t *Table) SetInstance(path string) {
|
||||
t.instance = path
|
||||
|
|
@ -162,7 +168,6 @@ func (t *Table) SetRefreshRate(d time.Duration) {
|
|||
|
||||
// ClusterWide checks if resource is scope for all namespaces.
|
||||
func (t *Table) ClusterWide() bool {
|
||||
log.Debug().Msgf("CLUSTER-WIDE %q", t.namespace)
|
||||
return client.IsClusterWide(t.namespace)
|
||||
}
|
||||
|
||||
|
|
@ -214,8 +219,9 @@ func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, err
|
|||
if !ok {
|
||||
return nil, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
|
||||
}
|
||||
a.Init(factory, client.NewGVR(t.gvr))
|
||||
a.Init(factory, t.gvr)
|
||||
|
||||
t.hasMetrics = factory.Client().HasMetrics()
|
||||
ns := client.CleanseNamespace(t.namespace)
|
||||
if client.IsClusterScoped(t.namespace) {
|
||||
ns = client.AllNamespaces
|
||||
|
|
@ -278,13 +284,13 @@ func (t *Table) getMeta(ctx context.Context) (ResourceMeta, error) {
|
|||
if !ok {
|
||||
return ResourceMeta{}, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
|
||||
}
|
||||
meta.DAO.Init(factory, client.NewGVR(t.gvr))
|
||||
meta.DAO.Init(factory, t.gvr)
|
||||
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
func (t *Table) resourceMeta() ResourceMeta {
|
||||
meta, ok := Registry[t.gvr]
|
||||
meta, ok := Registry[t.gvr.String()]
|
||||
if !ok {
|
||||
log.Debug().Msgf("Resource %s not found in registry. Going generic!", t.gvr)
|
||||
meta = ResourceMeta{
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import (
|
|||
)
|
||||
|
||||
func TestTableReconcile(t *testing.T) {
|
||||
ta := NewTable("v1/pods")
|
||||
ta := NewTable(client.NewGVR("v1/pods"))
|
||||
ta.SetNamespace(client.NamespaceAll)
|
||||
|
||||
f := makeFactory()
|
||||
|
|
@ -39,7 +39,7 @@ func TestTableReconcile(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTableList(t *testing.T) {
|
||||
ta := NewTable("v1/pods")
|
||||
ta := NewTable(client.NewGVR("v1/pods"))
|
||||
ta.SetNamespace("blee")
|
||||
|
||||
acc := accessor{}
|
||||
|
|
@ -50,7 +50,7 @@ func TestTableList(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTableGet(t *testing.T) {
|
||||
ta := NewTable("v1/pods")
|
||||
ta := NewTable(client.NewGVR("v1/pods"))
|
||||
ta.SetNamespace("blee")
|
||||
|
||||
f := makeFactory()
|
||||
|
|
@ -90,7 +90,7 @@ func TestTableMeta(t *testing.T) {
|
|||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
ta := NewTable(u.gvr)
|
||||
ta := NewTable(client.NewGVR(u.gvr))
|
||||
m := ta.resourceMeta()
|
||||
|
||||
assert.Equal(t, u.accessor, m.DAO)
|
||||
|
|
@ -106,7 +106,7 @@ func TestTableHydrate(t *testing.T) {
|
|||
|
||||
assert.Nil(t, hydrate("blee", oo, rr, render.Pod{}))
|
||||
assert.Equal(t, 1, len(rr))
|
||||
assert.Equal(t, 16, len(rr[0].Fields))
|
||||
assert.Equal(t, 17, len(rr[0].Fields))
|
||||
}
|
||||
|
||||
func TestTableGenericHydrate(t *testing.T) {
|
||||
|
|
@ -179,7 +179,7 @@ type testFactory struct {
|
|||
var _ dao.Factory = testFactory{}
|
||||
|
||||
func (f testFactory) Client() client.Connection {
|
||||
return nil
|
||||
return client.NewTestClient()
|
||||
}
|
||||
func (f testFactory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) {
|
||||
if len(f.rows) > 0 {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import (
|
|||
)
|
||||
|
||||
func TestTableRefresh(t *testing.T) {
|
||||
ta := model.NewTable("v1/pods")
|
||||
ta := model.NewTable(client.NewGVR("v1/pods"))
|
||||
ta.SetNamespace(client.NamespaceAll)
|
||||
|
||||
l := tableListener{}
|
||||
|
|
@ -41,7 +41,7 @@ func TestTableRefresh(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTableNS(t *testing.T) {
|
||||
ta := model.NewTable("v1/pods")
|
||||
ta := model.NewTable(client.NewGVR("v1/pods"))
|
||||
ta.SetNamespace("blee")
|
||||
|
||||
assert.Equal(t, "blee", ta.GetNamespace())
|
||||
|
|
@ -50,7 +50,7 @@ func TestTableNS(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTableAddListener(t *testing.T) {
|
||||
ta := model.NewTable("v1/pods")
|
||||
ta := model.NewTable(client.NewGVR("v1/pods"))
|
||||
ta.SetNamespace("blee")
|
||||
|
||||
assert.True(t, ta.Empty())
|
||||
|
|
@ -59,7 +59,7 @@ func TestTableAddListener(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTableRmListener(t *testing.T) {
|
||||
ta := model.NewTable("v1/pods")
|
||||
ta := model.NewTable(client.NewGVR("v1/pods"))
|
||||
ta.SetNamespace("blee")
|
||||
|
||||
l := tableListener{}
|
||||
|
|
@ -86,7 +86,7 @@ type tableFactory struct {
|
|||
var _ dao.Factory = tableFactory{}
|
||||
|
||||
func (f tableFactory) Client() client.Connection {
|
||||
return nil
|
||||
return client.NewTestClient()
|
||||
}
|
||||
func (f tableFactory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) {
|
||||
if len(f.rows) > 0 {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ type TreeListener interface {
|
|||
|
||||
// Tree represents a tree model.
|
||||
type Tree struct {
|
||||
gvr string
|
||||
gvr client.GVR
|
||||
namespace string
|
||||
root *xray.TreeNode
|
||||
listeners []TreeListener
|
||||
|
|
@ -41,7 +41,7 @@ type Tree struct {
|
|||
}
|
||||
|
||||
// NewTree returns a new model.
|
||||
func NewTree(gvr string) *Tree {
|
||||
func NewTree(gvr client.GVR) *Tree {
|
||||
return &Tree{
|
||||
gvr: gvr,
|
||||
refreshRate: 2 * time.Second,
|
||||
|
|
@ -193,7 +193,7 @@ func (t *Tree) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, erro
|
|||
if !ok {
|
||||
return nil, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
|
||||
}
|
||||
a.Init(factory, client.NewGVR(t.gvr))
|
||||
a.Init(factory, t.gvr)
|
||||
|
||||
return a.List(ctx, client.CleanseNamespace(t.namespace))
|
||||
}
|
||||
|
|
@ -206,7 +206,7 @@ func (t *Tree) reconcile(ctx context.Context) error {
|
|||
}
|
||||
|
||||
ns := client.CleanseNamespace(t.namespace)
|
||||
res := client.NewGVR(t.gvr).R()
|
||||
res := t.gvr.R()
|
||||
root := xray.NewTreeNode(res, res)
|
||||
ctx = context.WithValue(ctx, xray.KeyParent, root)
|
||||
if _, ok := meta.TreeRenderer.(*xray.Generic); ok {
|
||||
|
|
@ -236,7 +236,7 @@ func (t *Tree) reconcile(ctx context.Context) error {
|
|||
}
|
||||
|
||||
func (t *Tree) resourceMeta() ResourceMeta {
|
||||
meta, ok := Registry[t.gvr]
|
||||
meta, ok := Registry[t.gvr.String()]
|
||||
if !ok {
|
||||
log.Debug().Msgf("Resource %s not found in registry. Going generic!", t.gvr)
|
||||
meta = ResourceMeta{
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ type Renderer interface {
|
|||
Render(o interface{}, ns string, row *render.Row) error
|
||||
|
||||
// Header returns the resource header.
|
||||
Header(ns string) render.HeaderRow
|
||||
Header(ns string) render.Header
|
||||
|
||||
// ColorerFunc returns a row colorer function.
|
||||
ColorerFunc() render.ColorerFunc
|
||||
|
|
|
|||
|
|
@ -15,17 +15,17 @@ type Alias struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Alias) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
return func(ns string, _ Header, re RowEvent) tcell.Color {
|
||||
return tcell.ColorMediumSpringGreen
|
||||
}
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Alias) Header(ns string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "RESOURCE"},
|
||||
Header{Name: "COMMAND"},
|
||||
Header{Name: "APIGROUP"},
|
||||
func (Alias) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "RESOURCE"},
|
||||
HeaderColumn{Name: "COMMAND"},
|
||||
HeaderColumn{Name: "APIGROUP"},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ import (
|
|||
|
||||
func TestAliasColorer(t *testing.T) {
|
||||
var a render.Alias
|
||||
|
||||
h := render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
}
|
||||
r := render.Row{ID: "g/v/r", Fields: render.Fields{"r", "blee", "g"}}
|
||||
uu := map[string]struct {
|
||||
ns string
|
||||
|
|
@ -36,16 +40,16 @@ func TestAliasColorer(t *testing.T) {
|
|||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, a.ColorerFunc()(u.ns, u.re))
|
||||
assert.Equal(t, u.e, a.ColorerFunc()(u.ns, h, u.re))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliasHeader(t *testing.T) {
|
||||
h := render.HeaderRow{
|
||||
render.Header{Name: "RESOURCE"},
|
||||
render.Header{Name: "COMMAND"},
|
||||
render.Header{Name: "APIGROUP"},
|
||||
h := render.Header{
|
||||
render.HeaderColumn{Name: "RESOURCE"},
|
||||
render.HeaderColumn{Name: "COMMAND"},
|
||||
render.HeaderColumn{Name: "APIGROUP"},
|
||||
}
|
||||
|
||||
var a render.Alias
|
||||
|
|
|
|||
|
|
@ -31,28 +31,27 @@ type Benchmark struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (b Benchmark) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := tcell.ColorPaleGreen
|
||||
if !Happy(ns, re.Row) {
|
||||
c = ErrColor
|
||||
return func(ns string, h Header, re RowEvent) tcell.Color {
|
||||
if !Happy(ns, h, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
return c
|
||||
return tcell.ColorPaleGreen
|
||||
}
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Benchmark) Header(ns string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAMESPACE"},
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "STATUS"},
|
||||
Header{Name: "TIME"},
|
||||
Header{Name: "REQ/S", Align: tview.AlignRight},
|
||||
Header{Name: "2XX", Align: tview.AlignRight},
|
||||
Header{Name: "4XX/5XX", Align: tview.AlignRight},
|
||||
Header{Name: "REPORT"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
func (Benchmark) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "STATUS"},
|
||||
HeaderColumn{Name: "TIME"},
|
||||
HeaderColumn{Name: "REQ/S", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "2XX", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "4XX/5XX", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "REPORT"},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ type Chart struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Chart) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
if !Happy(ns, re.Row) {
|
||||
return func(ns string, h Header, re RowEvent) tcell.Color {
|
||||
if !Happy(ns, h, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
|
|
@ -27,21 +27,17 @@ func (Chart) ColorerFunc() ColorerFunc {
|
|||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Chart) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
func (Chart) Header(_ string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "REVISION"},
|
||||
HeaderColumn{Name: "STATUS"},
|
||||
HeaderColumn{Name: "CHART"},
|
||||
HeaderColumn{Name: "APP VERSION"},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "REVISION"},
|
||||
Header{Name: "STATUS"},
|
||||
Header{Name: "CHART"},
|
||||
Header{Name: "APP VERSION"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a chart to screen.
|
||||
|
|
@ -52,19 +48,16 @@ func (c Chart) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
|
||||
r.ID = client.FQN(h.Release.Namespace, h.Release.Name)
|
||||
r.Fields = make(Fields, 0, len(c.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, h.Release.Namespace)
|
||||
}
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
h.Release.Namespace,
|
||||
h.Release.Name,
|
||||
strconv.Itoa(h.Release.Version),
|
||||
h.Release.Info.Status.String(),
|
||||
h.Release.Chart.Metadata.Name+"-"+h.Release.Chart.Metadata.Version,
|
||||
h.Release.Chart.Metadata.Name + "-" + h.Release.Chart.Metadata.Version,
|
||||
h.Release.Chart.Metadata.AppVersion,
|
||||
asStatus(c.diagnose(h.Release.Info.Status.String())),
|
||||
toAge(metav1.Time{Time: h.Release.Info.LastDeployed.Time}),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,34 +5,43 @@ import "github.com/gdamore/tcell"
|
|||
var (
|
||||
// ModColor row modified color.
|
||||
ModColor tcell.Color
|
||||
|
||||
// AddColor row added color.
|
||||
AddColor tcell.Color
|
||||
|
||||
// ErrColor row err color.
|
||||
ErrColor tcell.Color
|
||||
|
||||
// StdColor row default color.
|
||||
StdColor tcell.Color
|
||||
|
||||
// HighlightColor row highlight color.
|
||||
HighlightColor tcell.Color
|
||||
|
||||
// KillColor row deleted color.
|
||||
KillColor tcell.Color
|
||||
|
||||
// CompletedColor row completed color.
|
||||
CompletedColor tcell.Color
|
||||
)
|
||||
|
||||
// ColorerFunc represents a resource row colorer.
|
||||
type ColorerFunc func(ns string, evt RowEvent) tcell.Color
|
||||
type ColorerFunc func(ns string, h Header, re RowEvent) tcell.Color
|
||||
|
||||
// DefaultColorer set the default table row colors.
|
||||
func DefaultColorer(ns string, evt RowEvent) tcell.Color {
|
||||
var col = StdColor
|
||||
switch evt.Kind {
|
||||
case EventAdd:
|
||||
col = AddColor
|
||||
case EventUpdate:
|
||||
col = ModColor
|
||||
case EventDelete:
|
||||
col = KillColor
|
||||
func DefaultColorer(ns string, h Header, re RowEvent) tcell.Color {
|
||||
if !Happy(ns, h, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return col
|
||||
switch re.Kind {
|
||||
case EventAdd:
|
||||
return AddColor
|
||||
case EventUpdate:
|
||||
return ModColor
|
||||
case EventDelete:
|
||||
return KillColor
|
||||
default:
|
||||
return StdColor
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
|
|
@ -37,17 +36,17 @@ type ContainerWithMetrics interface {
|
|||
// Container renders a K8s Container to screen.
|
||||
type Container struct{}
|
||||
|
||||
const readyCol = 2
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (c Container) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
color := DefaultColorer(ns, re)
|
||||
|
||||
if !Happy(ns, re.Row) {
|
||||
color = ErrColor
|
||||
return func(ns string, h Header, re RowEvent) tcell.Color {
|
||||
if !Happy(ns, h, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
stateCol := h.IndexOf("STATE", true)
|
||||
if stateCol == -1 {
|
||||
return DefaultColorer(ns, h, re)
|
||||
}
|
||||
stateCol := readyCol + 1
|
||||
switch strings.TrimSpace(re.Row.Fields[stateCol]) {
|
||||
case ContainerCreating, PodInitializing:
|
||||
return AddColor
|
||||
|
|
@ -56,33 +55,32 @@ func (c Container) ColorerFunc() ColorerFunc {
|
|||
case Completed:
|
||||
return CompletedColor
|
||||
case Running:
|
||||
return DefaultColorer(ns, h, re)
|
||||
default:
|
||||
color = ErrColor
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return color
|
||||
}
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Container) Header(ns string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "IMAGE"},
|
||||
Header{Name: "READY"},
|
||||
Header{Name: "STATE"},
|
||||
Header{Name: "INIT"},
|
||||
Header{Name: "RS", Align: tview.AlignRight},
|
||||
Header{Name: "PROBES(L:R)"},
|
||||
Header{Name: "CPU", Align: tview.AlignRight},
|
||||
Header{Name: "MEM", Align: tview.AlignRight},
|
||||
Header{Name: "%CPU/R", Align: tview.AlignRight},
|
||||
Header{Name: "%MEM/R", Align: tview.AlignRight},
|
||||
Header{Name: "%CPU/L", Align: tview.AlignRight},
|
||||
Header{Name: "%MEM/L", Align: tview.AlignRight},
|
||||
Header{Name: "PORTS"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
func (Container) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "IMAGE"},
|
||||
HeaderColumn{Name: "READY"},
|
||||
HeaderColumn{Name: "STATE"},
|
||||
HeaderColumn{Name: "INIT"},
|
||||
HeaderColumn{Name: "RS", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "PROBES(L:R)"},
|
||||
HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true},
|
||||
HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true},
|
||||
HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true},
|
||||
HeaderColumn{Name: "%MEM/R", Align: tview.AlignRight, MX: true},
|
||||
HeaderColumn{Name: "%CPU/L", Align: tview.AlignRight, MX: true},
|
||||
HeaderColumn{Name: "%MEM/L", Align: tview.AlignRight, MX: true},
|
||||
HeaderColumn{Name: "PORTS"},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -96,29 +94,28 @@ func (c Container) Render(o interface{}, name string, r *Row) error {
|
|||
cur, perc, limit := gatherMetrics(co.Container, co.MX)
|
||||
ready, state, restarts := "false", MissingValue, "0"
|
||||
if co.Status != nil {
|
||||
ready, state, restarts = boolToStr(co.Status.Ready), toState(co.Status.State), strconv.Itoa(int(co.Status.RestartCount))
|
||||
ready, state, restarts = boolToStr(co.Status.Ready), ToContainerState(co.Status.State), strconv.Itoa(int(co.Status.RestartCount))
|
||||
}
|
||||
|
||||
r.ID = co.Container.Name
|
||||
r.Fields = make(Fields, 0, len(c.Header(client.AllNamespaces)))
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
co.Container.Name,
|
||||
co.Container.Image,
|
||||
ready,
|
||||
state,
|
||||
boolToStr(co.IsInit),
|
||||
restarts,
|
||||
probe(co.Container.LivenessProbe)+":"+probe(co.Container.ReadinessProbe),
|
||||
probe(co.Container.LivenessProbe) + ":" + probe(co.Container.ReadinessProbe),
|
||||
cur.cpu,
|
||||
cur.mem,
|
||||
perc.cpu,
|
||||
perc.mem,
|
||||
limit.cpu,
|
||||
limit.mem,
|
||||
toStrPorts(co.Container.Ports),
|
||||
ToContainerPorts(co.Container.Ports),
|
||||
asStatus(c.diagnose(state, ready)),
|
||||
toAge(co.Age),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -170,7 +167,7 @@ func gatherMetrics(co *v1.Container, mx *mv1beta1.ContainerMetrics) (c, p, l met
|
|||
return
|
||||
}
|
||||
|
||||
func toStrPorts(pp []v1.ContainerPort) string {
|
||||
func ToContainerPorts(pp []v1.ContainerPort) string {
|
||||
ports := make([]string, len(pp))
|
||||
for i, p := range pp {
|
||||
if len(p.Name) > 0 {
|
||||
|
|
@ -185,7 +182,7 @@ func toStrPorts(pp []v1.ContainerPort) string {
|
|||
return strings.Join(ports, ",")
|
||||
}
|
||||
|
||||
func toState(s v1.ContainerState) string {
|
||||
func ToContainerState(s v1.ContainerState) string {
|
||||
switch {
|
||||
case s.Waiting != nil:
|
||||
if s.Waiting.Reason != "" {
|
||||
|
|
|
|||
|
|
@ -16,13 +16,13 @@ type Context struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Context) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
return func(ns string, h Header, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, h, r)
|
||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") {
|
||||
c = HighlightColor
|
||||
return HighlightColor
|
||||
}
|
||||
|
||||
return c
|
||||
|
|
@ -30,12 +30,12 @@ func (Context) ColorerFunc() ColorerFunc {
|
|||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Context) Header(ns string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "CLUSTER"},
|
||||
Header{Name: "AUTHINFO"},
|
||||
Header{Name: "NAMESPACE"},
|
||||
func (Context) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "CLUSTER"},
|
||||
HeaderColumn{Name: "AUTHINFO"},
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ func (ClusterRole) ColorerFunc() ColorerFunc {
|
|||
}
|
||||
|
||||
// Header returns a header rbw.
|
||||
func (ClusterRole) Header(string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
func (ClusterRole) Header(string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ func (ClusterRoleBinding) ColorerFunc() ColorerFunc {
|
|||
}
|
||||
|
||||
// Header returns a header rbw.
|
||||
func (ClusterRoleBinding) Header(string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "CLUSTERROLE"},
|
||||
Header{Name: "SUBJECT-KIND"},
|
||||
Header{Name: "SUBJECTS"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
func (ClusterRoleBinding) Header(string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "CLUSTERROLE"},
|
||||
HeaderColumn{Name: "SUBJECT-KIND"},
|
||||
HeaderColumn{Name: "SUBJECTS"},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@ func (CustomResourceDefinition) ColorerFunc() ColorerFunc {
|
|||
}
|
||||
|
||||
// Header returns a header rbw.
|
||||
func (CustomResourceDefinition) Header(string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
func (CustomResourceDefinition) Header(string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,25 +22,21 @@ func (CronJob) ColorerFunc() ColorerFunc {
|
|||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (CronJob) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
func (CronJob) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "SCHEDULE"},
|
||||
HeaderColumn{Name: "SUSPEND"},
|
||||
HeaderColumn{Name: "ACTIVE"},
|
||||
HeaderColumn{Name: "LAST_SCHEDULE"},
|
||||
HeaderColumn{Name: "SELECTOR", Wide: true},
|
||||
HeaderColumn{Name: "CONTAINERS", Wide: true},
|
||||
HeaderColumn{Name: "IMAGES", Wide: true},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "SCHEDULE"},
|
||||
Header{Name: "SUSPEND"},
|
||||
Header{Name: "ACTIVE"},
|
||||
Header{Name: "LAST_SCHEDULE"},
|
||||
Header{Name: "SELECTOR", Wide: true},
|
||||
Header{Name: "CONTAINERS", Wide: true},
|
||||
Header{Name: "IMAGES", Wide: true},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
|
|
@ -61,11 +57,8 @@ func (c CronJob) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
|
||||
r.ID = client.MetaFQN(cj.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(c.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, cj.Namespace)
|
||||
}
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
cj.Namespace,
|
||||
cj.Name,
|
||||
cj.Spec.Schedule,
|
||||
boolPtrToStr(cj.Spec.Suspend),
|
||||
|
|
@ -77,7 +70,7 @@ func (c CronJob) Render(o interface{}, ns string, r *Row) error {
|
|||
mapToStr(cj.Labels),
|
||||
"",
|
||||
toAge(cj.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// DeltaRow represents a collection of row detlas between old and new row.
|
||||
type DeltaRow []string
|
||||
|
||||
|
|
@ -20,6 +24,38 @@ func NewDeltaRow(o, n Row, excludeLast bool) DeltaRow {
|
|||
return deltas
|
||||
}
|
||||
|
||||
// Diff returns true if deltas differ or false otherwise.
|
||||
func (d DeltaRow) Diff(r DeltaRow, ageCol int) bool {
|
||||
if len(d) != len(r) {
|
||||
return true
|
||||
}
|
||||
|
||||
if ageCol < 0 || ageCol >= len(d) {
|
||||
return !reflect.DeepEqual(d, r)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(d[:ageCol], r[:ageCol]) {
|
||||
return true
|
||||
}
|
||||
if ageCol+1 >= len(d) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !reflect.DeepEqual(d[ageCol+1:], r[ageCol+1:])
|
||||
}
|
||||
|
||||
// Customize returns a subset of deltas.
|
||||
func (d DeltaRow) Customize(cols []int, out DeltaRow) {
|
||||
if d.IsBlank() {
|
||||
return
|
||||
}
|
||||
for i, c := range cols {
|
||||
if c < len(d) {
|
||||
out[i] = d[c]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IsBlank asserts a row has no values in it.
|
||||
func (d DeltaRow) IsBlank() bool {
|
||||
if len(d) == 0 {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,85 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDelta(t *testing.T) {
|
||||
func TestDeltaCustomize(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
r1, r2 render.Row
|
||||
cols []int
|
||||
e render.DeltaRow
|
||||
}{
|
||||
"same": {
|
||||
r1: render.Row{
|
||||
Fields: render.Fields{"a", "b", "c"},
|
||||
},
|
||||
r2: render.Row{
|
||||
Fields: render.Fields{"a", "b", "c"},
|
||||
},
|
||||
cols: []int{0, 1, 2},
|
||||
e: render.DeltaRow{"", "", ""},
|
||||
},
|
||||
"empty": {
|
||||
r1: render.Row{
|
||||
Fields: render.Fields{"a", "b", "c"},
|
||||
},
|
||||
r2: render.Row{
|
||||
Fields: render.Fields{"a", "b", "c"},
|
||||
},
|
||||
e: render.DeltaRow{},
|
||||
},
|
||||
"diff-full": {
|
||||
r1: render.Row{
|
||||
Fields: render.Fields{"a", "b", "c"},
|
||||
},
|
||||
r2: render.Row{
|
||||
Fields: render.Fields{"a1", "b1", "c1"},
|
||||
},
|
||||
cols: []int{0, 1, 2},
|
||||
e: render.DeltaRow{"a", "b", "c"},
|
||||
},
|
||||
"diff-reverse": {
|
||||
r1: render.Row{
|
||||
Fields: render.Fields{"a", "b", "c"},
|
||||
},
|
||||
r2: render.Row{
|
||||
Fields: render.Fields{"a1", "b1", "c1"},
|
||||
},
|
||||
cols: []int{2, 1, 0},
|
||||
e: render.DeltaRow{"c", "b", "a"},
|
||||
},
|
||||
"diff-skip": {
|
||||
r1: render.Row{
|
||||
Fields: render.Fields{"a", "b", "c"},
|
||||
},
|
||||
r2: render.Row{
|
||||
Fields: render.Fields{"a1", "b1", "c1"},
|
||||
},
|
||||
cols: []int{2, 0},
|
||||
e: render.DeltaRow{"c", "a"},
|
||||
},
|
||||
"diff-missing": {
|
||||
r1: render.Row{
|
||||
Fields: render.Fields{"a", "b", "c"},
|
||||
},
|
||||
r2: render.Row{
|
||||
Fields: render.Fields{"a1", "b1", "c1"},
|
||||
},
|
||||
cols: []int{2, 10, 0},
|
||||
e: render.DeltaRow{"c", "", "a"},
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
d := render.NewDeltaRow(u.r1, u.r2, false)
|
||||
out := make(render.DeltaRow, len(u.cols))
|
||||
d.Customize(u.cols, out)
|
||||
assert.Equal(t, u.e, out)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeltaNew(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
o render.Row
|
||||
n render.Row
|
||||
|
|
@ -54,11 +132,11 @@ func TestDelta(t *testing.T) {
|
|||
}
|
||||
|
||||
for k := range uu {
|
||||
uc := uu[k]
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
d := render.NewDeltaRow(uc.o, uc.n, false)
|
||||
assert.Equal(t, uc.e, d)
|
||||
assert.Equal(t, uc.blank, d.IsBlank())
|
||||
d := render.NewDeltaRow(u.o, u.n, false)
|
||||
assert.Equal(t, u.e, d)
|
||||
assert.Equal(t, u.blank, d.IsBlank())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -82,9 +160,52 @@ func TestDeltaBlank(t *testing.T) {
|
|||
}
|
||||
|
||||
for k := range uu {
|
||||
uc := uu[k]
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, uc.e, uc.r.IsBlank())
|
||||
assert.Equal(t, u.e, u.r.IsBlank())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeltaDiff(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
d1, d2 render.DeltaRow
|
||||
ageCol int
|
||||
e bool
|
||||
}{
|
||||
"empty": {
|
||||
d1: render.DeltaRow{"f1", "f2", "f3"},
|
||||
ageCol: 2,
|
||||
e: true,
|
||||
},
|
||||
"same": {
|
||||
d1: render.DeltaRow{"f1", "f2", "f3"},
|
||||
d2: render.DeltaRow{"f1", "f2", "f3"},
|
||||
ageCol: -1,
|
||||
},
|
||||
"diff": {
|
||||
d1: render.DeltaRow{"f1", "f2", "f3"},
|
||||
d2: render.DeltaRow{"f1", "f2", "f13"},
|
||||
ageCol: -1,
|
||||
e: true,
|
||||
},
|
||||
"diff-age-first": {
|
||||
d1: render.DeltaRow{"f1", "f2", "f3"},
|
||||
d2: render.DeltaRow{"f1", "f2", "f13"},
|
||||
ageCol: 0,
|
||||
e: true,
|
||||
},
|
||||
"diff-age-last": {
|
||||
d1: render.DeltaRow{"f1", "f2", "f3"},
|
||||
d2: render.DeltaRow{"f1", "f2", "f13"},
|
||||
ageCol: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, u.d1.Diff(u.d2, u.ageCol))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
|
@ -17,36 +16,22 @@ type Deployment struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (d Deployment) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
if !Happy(ns, r.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return StdColor
|
||||
}
|
||||
return DefaultColorer
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Deployment) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
func (Deployment) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "READY"},
|
||||
HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "READY", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "READY"},
|
||||
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
|
||||
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
||||
Header{Name: "READY", Align: tview.AlignRight},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
|
|
@ -63,20 +48,17 @@ func (d Deployment) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
|
||||
r.ID = client.MetaFQN(dp.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(d.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, dp.Namespace)
|
||||
}
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
dp.Namespace,
|
||||
dp.Name,
|
||||
strconv.Itoa(int(dp.Status.AvailableReplicas))+"/"+strconv.Itoa(int(dp.Status.Replicas)),
|
||||
strconv.Itoa(int(dp.Status.AvailableReplicas)) + "/" + strconv.Itoa(int(dp.Status.Replicas)),
|
||||
strconv.Itoa(int(dp.Status.UpdatedReplicas)),
|
||||
strconv.Itoa(int(dp.Status.AvailableReplicas)),
|
||||
strconv.Itoa(int(dp.Status.ReadyReplicas)),
|
||||
mapToStr(dp.Labels),
|
||||
asStatus(d.diagnose(dp.Status.Replicas, dp.Status.AvailableReplicas)),
|
||||
toAge(dp.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
|
@ -17,38 +16,23 @@ type DaemonSet struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (d DaemonSet) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
|
||||
if !Happy(ns, r.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return StdColor
|
||||
}
|
||||
return DefaultColorer
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (DaemonSet) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
func (DaemonSet) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "DESIRED", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "CURRENT", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "READY", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "DESIRED", Align: tview.AlignRight},
|
||||
Header{Name: "CURRENT", Align: tview.AlignRight},
|
||||
Header{Name: "READY", Align: tview.AlignRight},
|
||||
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
|
||||
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
|
|
@ -64,11 +48,8 @@ func (d DaemonSet) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
|
||||
r.ID = client.MetaFQN(ds.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(d.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, ds.Namespace)
|
||||
}
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
ds.Namespace,
|
||||
ds.Name,
|
||||
strconv.Itoa(int(ds.Status.DesiredNumberScheduled)),
|
||||
strconv.Itoa(int(ds.Status.CurrentNumberScheduled)),
|
||||
|
|
@ -78,7 +59,7 @@ func (d DaemonSet) Render(o interface{}, ns string, r *Row) error {
|
|||
mapToStr(ds.Labels),
|
||||
asStatus(d.diagnose(ds.Status.DesiredNumberScheduled, ds.Status.NumberReady)),
|
||||
toAge(ds.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,17 +20,13 @@ func (Endpoints) ColorerFunc() ColorerFunc {
|
|||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Endpoints) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
func (Endpoints) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "ENDPOINTS"},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "ENDPOINTS"},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
|
|
@ -47,14 +43,12 @@ func (e Endpoints) Render(o interface{}, ns string, r *Row) error {
|
|||
|
||||
r.ID = client.MetaFQN(ep.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(e.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, ep.Namespace)
|
||||
}
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
ep.Namespace,
|
||||
ep.Name,
|
||||
missing(toEPs(ep.Subsets)),
|
||||
toAge(ep.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,42 +19,35 @@ type Event struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (e Event) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
|
||||
if !Happy(ns, r.Row) {
|
||||
return func(ns string, h Header, re RowEvent) tcell.Color {
|
||||
if !Happy(ns, h, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
markCol := 3
|
||||
if !client.IsAllNamespaces(ns) {
|
||||
markCol = 2
|
||||
reasonCol := h.IndexOf("REASON", true)
|
||||
if reasonCol == -1 {
|
||||
return DefaultColorer(ns, h, re)
|
||||
}
|
||||
if strings.TrimSpace(r.Row.Fields[markCol]) == "Killing" {
|
||||
if strings.TrimSpace(re.Row.Fields[reasonCol]) == "Killing" {
|
||||
return KillColor
|
||||
}
|
||||
|
||||
return c
|
||||
return DefaultColorer(ns, h, re)
|
||||
}
|
||||
}
|
||||
|
||||
// Header returns a header rbw.
|
||||
func (Event) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
func (Event) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "TYPE"},
|
||||
HeaderColumn{Name: "REASON"},
|
||||
HeaderColumn{Name: "SOURCE"},
|
||||
HeaderColumn{Name: "COUNT", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "MESSAGE", Wide: true},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "TYPE"},
|
||||
Header{Name: "REASON"},
|
||||
Header{Name: "SOURCE"},
|
||||
Header{Name: "COUNT", Align: tview.AlignRight},
|
||||
Header{Name: "MESSAGE", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
|
|
@ -70,11 +63,8 @@ func (e Event) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
|
||||
r.ID = client.MetaFQN(ev.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(e.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, ev.Namespace)
|
||||
}
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
ev.Namespace,
|
||||
asRef(ev.InvolvedObject),
|
||||
ev.Type,
|
||||
ev.Reason,
|
||||
|
|
@ -82,7 +72,8 @@ func (e Event) Render(o interface{}, ns string, r *Row) error {
|
|||
strconv.Itoa(int(ev.Count)),
|
||||
ev.Message,
|
||||
asStatus(e.diagnose(ev.Type)),
|
||||
toAge(ev.LastTimestamp))
|
||||
toAge(ev.LastTimestamp),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,24 +35,20 @@ func (Generic) ColorerFunc() ColorerFunc {
|
|||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (g *Generic) Header(ns string) HeaderRow {
|
||||
func (g *Generic) Header(ns string) Header {
|
||||
if g.table == nil {
|
||||
return HeaderRow{}
|
||||
}
|
||||
|
||||
h := make(HeaderRow, 0, len(g.table.ColumnDefinitions))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
return Header{}
|
||||
}
|
||||
h := make(Header, 0, len(g.table.ColumnDefinitions))
|
||||
for i, c := range g.table.ColumnDefinitions {
|
||||
if c.Name == ageTableCol {
|
||||
g.ageIndex = i
|
||||
continue
|
||||
}
|
||||
h = append(h, Header{Name: strings.ToUpper(c.Name)})
|
||||
h = append(h, HeaderColumn{Name: strings.ToUpper(c.Name)})
|
||||
}
|
||||
if g.ageIndex > 0 {
|
||||
h = append(h, Header{Name: "AGE"})
|
||||
h = append(h, HeaderColumn{Name: "AGE", Time: true,})
|
||||
}
|
||||
|
||||
return h
|
||||
|
|
|
|||
|
|
@ -16,41 +16,41 @@ func TestGenericRender(t *testing.T) {
|
|||
table *metav1beta1.Table
|
||||
eID string
|
||||
eFields render.Fields
|
||||
eHeader render.HeaderRow
|
||||
eHeader render.Header
|
||||
}{
|
||||
"withNS": {
|
||||
ns: "ns1",
|
||||
table: makeNSGeneric(),
|
||||
eID: "ns1/c1",
|
||||
eFields: render.Fields{"c1", "c2", "c3"},
|
||||
eHeader: render.HeaderRow{
|
||||
render.Header{Name: "A"},
|
||||
render.Header{Name: "B"},
|
||||
render.Header{Name: "C"},
|
||||
eHeader: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
},
|
||||
"nsAll": {
|
||||
"all": {
|
||||
ns: client.NamespaceAll,
|
||||
table: makeNSGeneric(),
|
||||
eID: "ns1/c1",
|
||||
eFields: render.Fields{"ns1", "c1", "c2", "c3"},
|
||||
eHeader: render.HeaderRow{
|
||||
render.Header{Name: "NAMESPACE"},
|
||||
render.Header{Name: "A"},
|
||||
render.Header{Name: "B"},
|
||||
render.Header{Name: "C"},
|
||||
eHeader: render.Header{
|
||||
// render.HeaderColumn{Name: "NAMESPACE"},
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
},
|
||||
"AllNS": {
|
||||
"allNS": {
|
||||
ns: client.AllNamespaces,
|
||||
table: makeNSGeneric(),
|
||||
eID: "ns1/c1",
|
||||
eFields: render.Fields{"ns1", "c1", "c2", "c3"},
|
||||
eHeader: render.HeaderRow{
|
||||
render.Header{Name: "NAMESPACE"},
|
||||
render.Header{Name: "A"},
|
||||
render.Header{Name: "B"},
|
||||
render.Header{Name: "C"},
|
||||
eHeader: render.Header{
|
||||
// render.HeaderColumn{Name: "NAMESPACE"},
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
},
|
||||
"clusterWide": {
|
||||
|
|
@ -58,10 +58,10 @@ func TestGenericRender(t *testing.T) {
|
|||
table: makeNoNSGeneric(),
|
||||
eID: "-/c1",
|
||||
eFields: render.Fields{"c1", "c2", "c3"},
|
||||
eHeader: render.HeaderRow{
|
||||
render.Header{Name: "A"},
|
||||
render.Header{Name: "B"},
|
||||
render.Header{Name: "C"},
|
||||
eHeader: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
},
|
||||
"age": {
|
||||
|
|
@ -69,10 +69,10 @@ func TestGenericRender(t *testing.T) {
|
|||
table: makeAgeGeneric(),
|
||||
eID: "-/c1",
|
||||
eFields: render.Fields{"c1", "c2", "Age"},
|
||||
eHeader: render.HeaderRow{
|
||||
render.Header{Name: "A"},
|
||||
render.Header{Name: "C"},
|
||||
render.Header{Name: "AGE"},
|
||||
eHeader: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
render.HeaderColumn{Name: "AGE", Time: true,},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,155 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const ageCol = "AGE"
|
||||
|
||||
// HeaderColumn represent a table header
|
||||
type HeaderColumn struct {
|
||||
Name string
|
||||
Align int
|
||||
Decorator DecoratorFunc
|
||||
Hide bool
|
||||
Wide bool
|
||||
MX bool
|
||||
Time bool
|
||||
}
|
||||
|
||||
// Clone copies a header.
|
||||
func (h HeaderColumn) Clone() HeaderColumn {
|
||||
return h
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Header represents a table header.
|
||||
type Header []HeaderColumn
|
||||
|
||||
// Clone duplicates a header.
|
||||
func (h Header) Clone() Header {
|
||||
header := make(Header, len(h))
|
||||
for i, c := range h {
|
||||
header[i] = c.Clone()
|
||||
}
|
||||
|
||||
return header
|
||||
}
|
||||
|
||||
// MapIndices returns a collection of mapped column indices based of the requested columns.
|
||||
func (h Header) MapIndices(cols []string, wide bool, ii []int) {
|
||||
cc := make(map[int]struct{}, len(cols))
|
||||
var lastIndex int
|
||||
log.Debug().Msgf("MAP %d -- %d ", len(cols), len(ii))
|
||||
for i, col := range cols {
|
||||
idx := h.IndexOf(col, true)
|
||||
ii[i], cc[idx] = idx, struct{}{}
|
||||
lastIndex = i
|
||||
}
|
||||
if !wide {
|
||||
return
|
||||
}
|
||||
|
||||
for i := range h {
|
||||
if _, ok := cc[i]; ok {
|
||||
continue
|
||||
}
|
||||
lastIndex++
|
||||
if lastIndex < len(ii) {
|
||||
ii[lastIndex] = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Customize builds a header from custom col definitions.
|
||||
func (h Header) Customize(cols []string, wide bool) Header {
|
||||
if len(cols) == 0 {
|
||||
return h
|
||||
}
|
||||
cc := make(Header, 0, len(h))
|
||||
xx := make(map[int]struct{}, len(h))
|
||||
for _, c := range cols {
|
||||
idx := h.IndexOf(c, true)
|
||||
if idx == -1 {
|
||||
log.Warn().Msgf("Column %s is not available on this resource", c)
|
||||
continue
|
||||
}
|
||||
xx[idx] = struct{}{}
|
||||
col := h[idx].Clone()
|
||||
col.Wide = false
|
||||
cc = append(cc, col)
|
||||
}
|
||||
|
||||
if !wide {
|
||||
return cc
|
||||
}
|
||||
|
||||
for i, c := range h {
|
||||
if _, ok := xx[i]; ok {
|
||||
continue
|
||||
}
|
||||
col := c.Clone()
|
||||
col.Wide = true
|
||||
cc = append(cc, col)
|
||||
}
|
||||
|
||||
return cc
|
||||
}
|
||||
|
||||
// Diff returns true if the header changed.
|
||||
func (h Header) Diff(header Header) bool {
|
||||
if len(h) != len(header) {
|
||||
return true
|
||||
}
|
||||
return !reflect.DeepEqual(h, header)
|
||||
}
|
||||
|
||||
// Columns return header as a collection of strings.
|
||||
func (h Header) Columns(wide bool) []string {
|
||||
if len(h) == 0 {
|
||||
return nil
|
||||
}
|
||||
var cc []string
|
||||
for _, c := range h {
|
||||
if !wide && c.Wide {
|
||||
continue
|
||||
}
|
||||
cc = append(cc, c.Name)
|
||||
}
|
||||
|
||||
return cc
|
||||
}
|
||||
|
||||
// HasAge returns true if table has an age column.
|
||||
func (h Header) HasAge() bool {
|
||||
return h.IndexOf(ageCol, true) != -1
|
||||
}
|
||||
|
||||
// AgeCol checks if given column index is the age column.
|
||||
func (h Header) IsAgeCol(col int) bool {
|
||||
if !h.HasAge() || col >= len(h) {
|
||||
return false
|
||||
}
|
||||
return h[col].Time
|
||||
}
|
||||
|
||||
// ValidColIndex returns the valid col index or -1 if none.
|
||||
func (h Header) ValidColIndex() int {
|
||||
return h.IndexOf("VALID", true)
|
||||
}
|
||||
|
||||
// IndexOf returns the col index or -1 if none.
|
||||
func (h Header) IndexOf(colName string, includeWide bool) int {
|
||||
for i, c := range h {
|
||||
if c.Wide && !includeWide {
|
||||
continue
|
||||
}
|
||||
if c.Name == colName {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
|
@ -0,0 +1,338 @@
|
|||
package render_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHeaderMapIndices(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
h1 render.Header
|
||||
cols []string
|
||||
wide bool
|
||||
e []int
|
||||
}{
|
||||
"all": {
|
||||
h1: makeHeader(),
|
||||
cols: []string{"A", "B", "C"},
|
||||
e: []int{0, 1, 2},
|
||||
},
|
||||
"reverse": {
|
||||
h1: makeHeader(),
|
||||
cols: []string{"C", "B", "A"},
|
||||
e: []int{2, 1, 0},
|
||||
},
|
||||
"missing": {
|
||||
h1: makeHeader(),
|
||||
cols: []string{"Duh", "B", "A"},
|
||||
e: []int{-1, 1, 0},
|
||||
},
|
||||
"skip": {
|
||||
h1: makeHeader(),
|
||||
cols: []string{"C", "A"},
|
||||
e: []int{2, 0},
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
ii := make([]int, len(u.cols))
|
||||
u.h1.MapIndices(u.cols, u.wide, ii)
|
||||
assert.Equal(t, u.e, ii)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderIndexOf(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
h render.Header
|
||||
name string
|
||||
wide bool
|
||||
e int
|
||||
}{
|
||||
"shown": {
|
||||
h: makeHeader(),
|
||||
name: "A",
|
||||
e: 0,
|
||||
},
|
||||
"hidden": {
|
||||
h: makeHeader(),
|
||||
name: "B",
|
||||
e: -1,
|
||||
},
|
||||
"hidden-wide": {
|
||||
h: makeHeader(),
|
||||
name: "B",
|
||||
wide: true,
|
||||
e: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, u.h.IndexOf(u.name, u.wide))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderCustomize(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
h render.Header
|
||||
cols []string
|
||||
wide bool
|
||||
e render.Header
|
||||
}{
|
||||
"default": {
|
||||
h: makeHeader(),
|
||||
e: makeHeader(),
|
||||
},
|
||||
"default-wide": {
|
||||
h: makeHeader(),
|
||||
wide: true,
|
||||
e: makeHeader(),
|
||||
},
|
||||
"reverse": {
|
||||
h: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B", Wide: true},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
cols: []string{"C", "A"},
|
||||
e: render.Header{
|
||||
render.HeaderColumn{Name: "C"},
|
||||
render.HeaderColumn{Name: "A"},
|
||||
},
|
||||
},
|
||||
"reverse-wide": {
|
||||
h: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B", Wide: true},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
cols: []string{"C", "A"},
|
||||
wide: true,
|
||||
e: render.Header{
|
||||
render.HeaderColumn{Name: "C"},
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B", Wide: true},
|
||||
},
|
||||
},
|
||||
"toggle-wide": {
|
||||
h: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B", Wide: true},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
cols: []string{"C", "B"},
|
||||
wide: true,
|
||||
e: render.Header{
|
||||
render.HeaderColumn{Name: "C"},
|
||||
render.HeaderColumn{Name: "B", Wide: false},
|
||||
render.HeaderColumn{Name: "A", Wide: true},
|
||||
},
|
||||
},
|
||||
"missing": {
|
||||
h: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B", Wide: true},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
cols: []string{"BLEE", "A"},
|
||||
wide: true,
|
||||
e: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B", Wide: true},
|
||||
render.HeaderColumn{Name: "C", Wide: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, u.h.Customize(u.cols, u.wide))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderDiff(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
h1, h2 render.Header
|
||||
e bool
|
||||
}{
|
||||
"same": {
|
||||
h1: makeHeader(),
|
||||
h2: makeHeader(),
|
||||
},
|
||||
"size": {
|
||||
h1: makeHeader(),
|
||||
h2: makeHeader()[1:],
|
||||
e: true,
|
||||
},
|
||||
"differ-wide": {
|
||||
h1: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B", Wide: true},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
h2: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
e: true,
|
||||
},
|
||||
"differ-order": {
|
||||
h1: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B", Wide: true},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
h2: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
render.HeaderColumn{Name: "B", Wide: true},
|
||||
},
|
||||
e: true,
|
||||
},
|
||||
"differ-name": {
|
||||
h1: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
},
|
||||
h2: render.Header{
|
||||
render.HeaderColumn{Name: "B"},
|
||||
},
|
||||
e: true,
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, u.h1.Diff(u.h2))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderHasAge(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
h render.Header
|
||||
age, e bool
|
||||
}{
|
||||
"no-age": {
|
||||
h: render.Header{},
|
||||
},
|
||||
"age": {
|
||||
h: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B", Wide: true},
|
||||
render.HeaderColumn{Name: "AGE", Time: true},
|
||||
},
|
||||
e: true,
|
||||
age: true,
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, u.h.HasAge())
|
||||
assert.Equal(t, u.e, u.h.IsAgeCol(2))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderValidColIndex(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
h render.Header
|
||||
e int
|
||||
}{
|
||||
"none": {
|
||||
h: render.Header{},
|
||||
e: -1,
|
||||
},
|
||||
"valid": {
|
||||
h: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B", Wide: true},
|
||||
render.HeaderColumn{Name: "VALID", Wide: true},
|
||||
},
|
||||
e: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, u.h.ValidColIndex())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderColumns(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
h render.Header
|
||||
wide bool
|
||||
e []string
|
||||
}{
|
||||
"empty": {
|
||||
h: render.Header{},
|
||||
},
|
||||
"regular": {
|
||||
h: makeHeader(),
|
||||
e: []string{"A", "C"},
|
||||
},
|
||||
"wide": {
|
||||
h: makeHeader(),
|
||||
e: []string{"A", "B", "C"},
|
||||
wide: true,
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, u.h.Columns(u.wide))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderClone(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
h render.Header
|
||||
}{
|
||||
"empty": {
|
||||
h: render.Header{},
|
||||
},
|
||||
"full": {
|
||||
h: makeHeader(),
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
c := u.h.Clone()
|
||||
assert.Equal(t, len(u.h), len(c))
|
||||
if len(u.h) > 0 {
|
||||
u.h[0].Name = "blee"
|
||||
assert.Equal(t, "A", c[0].Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func makeHeader() render.Header {
|
||||
return render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B", Wide: true},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
}
|
||||
}
|
||||
|
|
@ -14,8 +14,14 @@ import (
|
|||
)
|
||||
|
||||
// Happy returns true if resoure is happy, false otherwise
|
||||
func Happy(ns string, r Row) bool {
|
||||
validCol := r.Len() - 2
|
||||
func Happy(ns string, h Header, r Row) bool {
|
||||
if len(r.Fields) == 0 {
|
||||
return true
|
||||
}
|
||||
validCol := h.IndexOf("VALID", true)
|
||||
if validCol < 0 {
|
||||
return true
|
||||
}
|
||||
return strings.TrimSpace(r.Fields[validCol]) == ""
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,22 +23,18 @@ func (HorizontalPodAutoscaler) ColorerFunc() ColorerFunc {
|
|||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (HorizontalPodAutoscaler) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
func (HorizontalPodAutoscaler) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "REFERENCE"},
|
||||
HeaderColumn{Name: "TARGETS%"},
|
||||
HeaderColumn{Name: "MINPODS", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "MAXPODS", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "REPLICAS", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "REFERENCE"},
|
||||
Header{Name: "TARGETS%"},
|
||||
Header{Name: "MINPODS", Align: tview.AlignRight},
|
||||
Header{Name: "MAXPODS", Align: tview.AlignRight},
|
||||
Header{Name: "REPLICAS", Align: tview.AlignRight},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
|
|
@ -62,7 +58,7 @@ func (h HorizontalPodAutoscaler) Render(o interface{}, ns string, r *Row) error
|
|||
}
|
||||
}
|
||||
|
||||
func (h HorizontalPodAutoscaler) renderV1(raw *unstructured.Unstructured, ns string, r *Row) error {
|
||||
func (h HorizontalPodAutoscaler) renderV1(raw *unstructured.Unstructured, _ string, r *Row) error {
|
||||
var hpa autoscalingv1.HorizontalPodAutoscaler
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &hpa)
|
||||
if err != nil {
|
||||
|
|
@ -70,11 +66,8 @@ func (h HorizontalPodAutoscaler) renderV1(raw *unstructured.Unstructured, ns str
|
|||
}
|
||||
|
||||
r.ID = client.MetaFQN(hpa.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(h.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, hpa.Namespace)
|
||||
}
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
hpa.Namespace,
|
||||
hpa.ObjectMeta.Name,
|
||||
hpa.Spec.ScaleTargetRef.Name,
|
||||
toMetricsV1(hpa.Spec, hpa.Status),
|
||||
|
|
@ -83,12 +76,12 @@ func (h HorizontalPodAutoscaler) renderV1(raw *unstructured.Unstructured, ns str
|
|||
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
||||
"",
|
||||
toAge(hpa.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h HorizontalPodAutoscaler) renderV2b1(raw *unstructured.Unstructured, ns string, r *Row) error {
|
||||
func (h HorizontalPodAutoscaler) renderV2b1(raw *unstructured.Unstructured, _ string, r *Row) error {
|
||||
var hpa autoscalingv2beta1.HorizontalPodAutoscaler
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &hpa)
|
||||
if err != nil {
|
||||
|
|
@ -96,12 +89,8 @@ func (h HorizontalPodAutoscaler) renderV2b1(raw *unstructured.Unstructured, ns s
|
|||
}
|
||||
|
||||
r.ID = client.MetaFQN(hpa.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(h.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, hpa.Namespace)
|
||||
}
|
||||
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
hpa.Namespace,
|
||||
hpa.ObjectMeta.Name,
|
||||
hpa.Spec.ScaleTargetRef.Name,
|
||||
toMetricsV2b1(hpa.Spec.Metrics, hpa.Status.CurrentMetrics),
|
||||
|
|
@ -110,12 +99,12 @@ func (h HorizontalPodAutoscaler) renderV2b1(raw *unstructured.Unstructured, ns s
|
|||
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
||||
"",
|
||||
toAge(hpa.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h HorizontalPodAutoscaler) renderV2b2(raw *unstructured.Unstructured, ns string, r *Row) error {
|
||||
func (h HorizontalPodAutoscaler) renderV2b2(raw *unstructured.Unstructured, _ string, r *Row) error {
|
||||
var hpa autoscalingv2beta2.HorizontalPodAutoscaler
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &hpa)
|
||||
if err != nil {
|
||||
|
|
@ -123,12 +112,8 @@ func (h HorizontalPodAutoscaler) renderV2b2(raw *unstructured.Unstructured, ns s
|
|||
}
|
||||
|
||||
r.ID = client.MetaFQN(hpa.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(h.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, hpa.Namespace)
|
||||
}
|
||||
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
hpa.Namespace,
|
||||
hpa.ObjectMeta.Name,
|
||||
hpa.Spec.ScaleTargetRef.Name,
|
||||
toMetricsV2b2(hpa.Spec.Metrics, hpa.Status.CurrentMetrics),
|
||||
|
|
@ -137,7 +122,7 @@ func (h HorizontalPodAutoscaler) renderV2b2(raw *unstructured.Unstructured, ns s
|
|||
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
||||
"",
|
||||
toAge(hpa.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,20 +20,16 @@ func (Ingress) ColorerFunc() ColorerFunc {
|
|||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Ingress) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
func (Ingress) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "HOSTS"},
|
||||
HeaderColumn{Name: "ADDRESS"},
|
||||
HeaderColumn{Name: "PORT"},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "HOSTS"},
|
||||
Header{Name: "ADDRESS"},
|
||||
Header{Name: "PORT"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
|
|
@ -49,18 +45,15 @@ func (i Ingress) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
|
||||
r.ID = client.MetaFQN(ing.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(i.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, ing.Namespace)
|
||||
}
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
ing.Namespace,
|
||||
ing.Name,
|
||||
toHosts(ing.Spec.Rules),
|
||||
toAddress(ing.Status.LoadBalancer),
|
||||
toTLSPorts(ing.Spec.TLS),
|
||||
"",
|
||||
toAge(ing.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,22 +24,18 @@ func (Job) ColorerFunc() ColorerFunc {
|
|||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Job) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
func (Job) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "COMPLETIONS"},
|
||||
HeaderColumn{Name: "DURATION"},
|
||||
HeaderColumn{Name: "SELECTOR", Wide: true},
|
||||
HeaderColumn{Name: "CONTAINERS", Wide: true},
|
||||
HeaderColumn{Name: "IMAGES", Wide: true},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "COMPLETIONS"},
|
||||
Header{Name: "DURATION"},
|
||||
Header{Name: "SELECTOR", Wide: true},
|
||||
Header{Name: "CONTAINERS", Wide: true},
|
||||
Header{Name: "IMAGES", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
|
|
@ -55,13 +51,11 @@ func (j Job) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
ready := toCompletion(job.Spec, job.Status)
|
||||
|
||||
r.ID = client.MetaFQN(job.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(j.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, job.Namespace)
|
||||
}
|
||||
cc, ii := toContainers(job.Spec.Template.Spec)
|
||||
r.Fields = append(r.Fields,
|
||||
|
||||
r.ID = client.MetaFQN(job.ObjectMeta)
|
||||
r.Fields = Fields{
|
||||
job.Namespace,
|
||||
job.Name,
|
||||
ready,
|
||||
toDuration(job.Status),
|
||||
|
|
@ -70,7 +64,7 @@ func (j Job) Render(o interface{}, ns string, r *Row) error {
|
|||
ii,
|
||||
asStatus(j.diagnose(ready, job.Status.CompletionTime)),
|
||||
toAge(job.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
|
@ -26,36 +25,28 @@ type Node struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (n Node) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if !Happy(ns, r.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
return DefaultColorer
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Node) Header(_ string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "STATUS"},
|
||||
Header{Name: "ROLE", Wide: true},
|
||||
Header{Name: "VERSION", Wide: true},
|
||||
Header{Name: "KERNEL", Wide: true},
|
||||
Header{Name: "INTERNAL-IP", Wide: true},
|
||||
Header{Name: "EXTERNAL-IP", Wide: true},
|
||||
Header{Name: "CPU", Align: tview.AlignRight},
|
||||
Header{Name: "MEM", Align: tview.AlignRight},
|
||||
Header{Name: "%CPU", Align: tview.AlignRight},
|
||||
Header{Name: "%MEM", Align: tview.AlignRight},
|
||||
Header{Name: "ACPU", Align: tview.AlignRight},
|
||||
Header{Name: "AMEM", Align: tview.AlignRight},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
func (Node) Header(_ string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "STATUS"},
|
||||
HeaderColumn{Name: "ROLE"},
|
||||
HeaderColumn{Name: "VERSION"},
|
||||
HeaderColumn{Name: "KERNEL", Wide: true},
|
||||
HeaderColumn{Name: "INTERNAL-IP", Wide: true},
|
||||
HeaderColumn{Name: "EXTERNAL-IP", Wide: true},
|
||||
HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true},
|
||||
HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true},
|
||||
HeaderColumn{Name: "%CPU", Align: tview.AlignRight, MX: true},
|
||||
HeaderColumn{Name: "%MEM", Align: tview.AlignRight, MX: true},
|
||||
HeaderColumn{Name: "ACPU", Align: tview.AlignRight, MX: true},
|
||||
HeaderColumn{Name: "AMEM", Align: tview.AlignRight, MX: true},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,8 +81,7 @@ func (n Node) Render(o interface{}, ns string, r *Row) error {
|
|||
sort.Sort(roles)
|
||||
|
||||
r.ID = client.FQN("", na)
|
||||
r.Fields = make(Fields, 0, len(n.Header(ns)))
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
no.Name,
|
||||
join(statuses, ","),
|
||||
join(roles, ","),
|
||||
|
|
@ -108,7 +98,7 @@ func (n Node) Render(o interface{}, ns string, r *Row) error {
|
|||
mapToStr(no.Labels),
|
||||
asStatus(n.diagnose(statuses)),
|
||||
toAge(no.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,24 +20,20 @@ func (NetworkPolicy) ColorerFunc() ColorerFunc {
|
|||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (NetworkPolicy) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
func (NetworkPolicy) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "ING-SELECTOR", Wide: true},
|
||||
HeaderColumn{Name: "ING-PORTS"},
|
||||
HeaderColumn{Name: "ING-BLOCK"},
|
||||
HeaderColumn{Name: "EGR-SELECTOR", Wide: true},
|
||||
HeaderColumn{Name: "EGR-PORTS"},
|
||||
HeaderColumn{Name: "EGR-BLOCK"},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "ING-SELECTOR", Wide: true},
|
||||
Header{Name: "ING-PORTS"},
|
||||
Header{Name: "ING-BLOCK"},
|
||||
Header{Name: "EGR-SELECTOR", Wide: true},
|
||||
Header{Name: "EGR-PORTS"},
|
||||
Header{Name: "EGR-BLOCK"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
|
|
@ -56,11 +52,8 @@ func (n NetworkPolicy) Render(o interface{}, ns string, r *Row) error {
|
|||
ep, es, eb := egress(np.Spec.Egress)
|
||||
|
||||
r.ID = client.MetaFQN(np.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(n.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, np.Namespace)
|
||||
}
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
np.Namespace,
|
||||
np.Name,
|
||||
is,
|
||||
ip,
|
||||
|
|
@ -71,7 +64,7 @@ func (n NetworkPolicy) Render(o interface{}, ns string, r *Row) error {
|
|||
mapToStr(np.Labels),
|
||||
"",
|
||||
toAge(np.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,34 +17,32 @@ type Namespace struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (n Namespace) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd {
|
||||
return c
|
||||
}
|
||||
|
||||
if r.Kind == EventUpdate {
|
||||
c = StdColor
|
||||
}
|
||||
if !Happy(ns, r.Row) {
|
||||
return func(ns string, h Header, re RowEvent) tcell.Color {
|
||||
if !Happy(ns, h, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") {
|
||||
c = HighlightColor
|
||||
if re.Kind == EventAdd {
|
||||
return DefaultColorer(ns, h, re)
|
||||
}
|
||||
if re.Kind == EventUpdate {
|
||||
return StdColor
|
||||
}
|
||||
if strings.Contains(strings.TrimSpace(re.Row.Fields[0]), "*") {
|
||||
return HighlightColor
|
||||
}
|
||||
|
||||
return c
|
||||
return DefaultColorer(ns, h, re)
|
||||
}
|
||||
}
|
||||
|
||||
// Header returns a header rbw.
|
||||
func (Namespace) Header(string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "STATUS"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
func (Namespace) Header(string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "STATUS"},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,10 +27,15 @@ func TestNSColorer(t *testing.T) {
|
|||
{"", render.RowEvent{Kind: render.EventUnchanged, Row: dead}, render.ErrColor},
|
||||
}
|
||||
|
||||
h := render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
}
|
||||
|
||||
var n render.Namespace
|
||||
f := n.ColorerFunc()
|
||||
for _, u := range uu {
|
||||
assert.Equal(t, u.e, f(u.ns, u.r))
|
||||
assert.Equal(t, u.e, f(u.ns, h, u.r))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ type OpenFaas struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (o OpenFaas) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
if !Happy(ns, re.Row) {
|
||||
return func(ns string, h Header, re RowEvent) tcell.Color {
|
||||
if !Happy(ns, h, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
|
|
@ -35,23 +35,19 @@ func (o OpenFaas) ColorerFunc() ColorerFunc {
|
|||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (OpenFaas) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
func (OpenFaas) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "STATUS"},
|
||||
HeaderColumn{Name: "IMAGE"},
|
||||
HeaderColumn{Name: "LABELS"},
|
||||
HeaderColumn{Name: "INVOCATIONS", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "REPLICAS", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "STATUS"},
|
||||
Header{Name: "IMAGE"},
|
||||
Header{Name: "LABELS"},
|
||||
Header{Name: "INVOCATIONS", Align: tview.AlignRight},
|
||||
Header{Name: "REPLICAS", Align: tview.AlignRight},
|
||||
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a chart to screen.
|
||||
|
|
@ -71,11 +67,8 @@ func (o OpenFaas) Render(i interface{}, ns string, r *Row) error {
|
|||
}
|
||||
|
||||
r.ID = client.FQN(fn.Function.Namespace, fn.Function.Name)
|
||||
r.Fields = make(Fields, 0, len(o.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, fn.Function.Namespace)
|
||||
}
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
fn.Function.Namespace,
|
||||
fn.Function.Name,
|
||||
status,
|
||||
fn.Function.Image,
|
||||
|
|
@ -85,7 +78,7 @@ func (o OpenFaas) Render(i interface{}, ns string, r *Row) error {
|
|||
strconv.Itoa(int(fn.Function.AvailableReplicas)),
|
||||
asStatus(o.diagnose(status)),
|
||||
toAge(metav1.Time{Time: time.Now()}),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
v1beta1 "k8s.io/api/policy/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
|
@ -18,39 +17,24 @@ type PodDisruptionBudget struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (p PodDisruptionBudget) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, re)
|
||||
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
|
||||
if !Happy(ns, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return StdColor
|
||||
}
|
||||
return DefaultColorer
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (PodDisruptionBudget) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
func (PodDisruptionBudget) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "MIN AVAILABLE", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "MAX_ UNAVAILABLE", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "ALLOWED DISRUPTIONS", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "CURRENT", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "DESIRED", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "EXPECTED", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "MIN AVAILABLE", Align: tview.AlignRight},
|
||||
Header{Name: "MAX_ UNAVAILABLE", Align: tview.AlignRight},
|
||||
Header{Name: "ALLOWED DISRUPTIONS", Align: tview.AlignRight},
|
||||
Header{Name: "CURRENT", Align: tview.AlignRight},
|
||||
Header{Name: "DESIRED", Align: tview.AlignRight},
|
||||
Header{Name: "EXPECTED", Align: tview.AlignRight},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
|
|
@ -66,11 +50,8 @@ func (p PodDisruptionBudget) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
|
||||
r.ID = client.MetaFQN(pdb.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(p.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, pdb.Namespace)
|
||||
}
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
pdb.Namespace,
|
||||
pdb.Name,
|
||||
numbToStr(pdb.Spec.MinAvailable),
|
||||
numbToStr(pdb.Spec.MaxUnavailable),
|
||||
|
|
@ -81,7 +62,7 @@ func (p PodDisruptionBudget) Render(o interface{}, ns string, r *Row) error {
|
|||
mapToStr(pdb.Labels),
|
||||
asStatus(p.diagnose(pdb.Spec.MinAvailable, pdb.Status.CurrentHealthy)),
|
||||
toAge(pdb.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,60 +22,53 @@ type Pod struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (p Pod) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, re)
|
||||
|
||||
statusCol := 4
|
||||
if !client.IsAllNamespaces(ns) {
|
||||
statusCol--
|
||||
return func(ns string, h Header, re RowEvent) tcell.Color {
|
||||
statusCol := h.IndexOf("STATUS", true)
|
||||
if statusCol == -1 {
|
||||
return DefaultColorer(ns, h, re)
|
||||
}
|
||||
status := strings.TrimSpace(re.Row.Fields[statusCol])
|
||||
switch status {
|
||||
case ContainerCreating, PodInitializing:
|
||||
c = AddColor
|
||||
return AddColor
|
||||
case Initialized:
|
||||
c = HighlightColor
|
||||
return HighlightColor
|
||||
case Completed:
|
||||
c = CompletedColor
|
||||
return CompletedColor
|
||||
case Running:
|
||||
c = StdColor
|
||||
return StdColor
|
||||
case Terminating:
|
||||
c = KillColor
|
||||
return KillColor
|
||||
default:
|
||||
if !Happy(ns, re.Row) {
|
||||
c = ErrColor
|
||||
if !Happy(ns, h, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
return DefaultColorer(ns, h, re)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Pod) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
func (Pod) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "READY"},
|
||||
HeaderColumn{Name: "RS", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "STATUS"},
|
||||
HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true},
|
||||
HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true},
|
||||
HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true},
|
||||
HeaderColumn{Name: "%MEM/R", Align: tview.AlignRight, MX: true},
|
||||
HeaderColumn{Name: "%CPU/L", Align: tview.AlignRight, MX: true},
|
||||
HeaderColumn{Name: "%MEM/L", Align: tview.AlignRight, MX: true},
|
||||
HeaderColumn{Name: "IP"},
|
||||
HeaderColumn{Name: "NODE"},
|
||||
HeaderColumn{Name: "QOS", Wide: true},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "READY"},
|
||||
Header{Name: "RS", Align: tview.AlignRight},
|
||||
Header{Name: "STATUS"},
|
||||
Header{Name: "CPU", Align: tview.AlignRight},
|
||||
Header{Name: "MEM", Align: tview.AlignRight},
|
||||
Header{Name: "%CPU/R", Align: tview.AlignRight},
|
||||
Header{Name: "%MEM/R", Align: tview.AlignRight},
|
||||
Header{Name: "%CPU/L", Align: tview.AlignRight},
|
||||
Header{Name: "%MEM/L", Align: tview.AlignRight},
|
||||
Header{Name: "IP", Wide: true},
|
||||
Header{Name: "NODE", Wide: true},
|
||||
Header{Name: "QOS", Wide: true},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
|
|
@ -96,13 +89,10 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
|||
c, perc := p.gatherPodMX(&po, pwm.MX)
|
||||
phase := p.Phase(&po)
|
||||
r.ID = client.MetaFQN(po.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(p.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, po.Namespace)
|
||||
}
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
po.Namespace,
|
||||
po.ObjectMeta.Name,
|
||||
strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)),
|
||||
strconv.Itoa(cr) + "/" + strconv.Itoa(len(ss)),
|
||||
strconv.Itoa(rc),
|
||||
phase,
|
||||
c.cpu,
|
||||
|
|
@ -117,7 +107,7 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
|||
mapToStr(po.Labels),
|
||||
asStatus(p.diagnose(phase, cr, len(ss))),
|
||||
toAge(po.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,10 +51,19 @@ func TestPodColorer(t *testing.T) {
|
|||
{"blee", render.RowEvent{Kind: render.EventUpdate, Row: notReady}, render.ErrColor},
|
||||
}
|
||||
|
||||
h := render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
render.HeaderColumn{Name: "D"},
|
||||
render.HeaderColumn{Name: "E"},
|
||||
render.HeaderColumn{Name: "F"},
|
||||
}
|
||||
|
||||
var p render.Pod
|
||||
f := p.ColorerFunc()
|
||||
for _, u := range uu {
|
||||
assert.Equal(t, u.e, f(u.ns, u.r))
|
||||
assert.Equal(t, u.e, f(u.ns, h, u.r))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,17 +9,17 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
func rbacVerbHeader() HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "GET "},
|
||||
Header{Name: "LIST "},
|
||||
Header{Name: "WATCH "},
|
||||
Header{Name: "CREATE"},
|
||||
Header{Name: "PATCH "},
|
||||
Header{Name: "UPDATE"},
|
||||
Header{Name: "DELETE"},
|
||||
Header{Name: "DEL-LIST "},
|
||||
Header{Name: "EXTRAS", Wide: true},
|
||||
func rbacVerbHeader() Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "GET "},
|
||||
HeaderColumn{Name: "LIST "},
|
||||
HeaderColumn{Name: "WATCH "},
|
||||
HeaderColumn{Name: "CREATE"},
|
||||
HeaderColumn{Name: "PATCH "},
|
||||
HeaderColumn{Name: "UPDATE"},
|
||||
HeaderColumn{Name: "DELETE"},
|
||||
HeaderColumn{Name: "DEL-LIST "},
|
||||
HeaderColumn{Name: "EXTRAS", Wide: true},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -28,21 +28,21 @@ type Policy struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Policy) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
return func(ns string, _ Header, re RowEvent) tcell.Color {
|
||||
return tcell.ColorMediumSpringGreen
|
||||
}
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Policy) Header(ns string) HeaderRow {
|
||||
h := HeaderRow{
|
||||
Header{Name: "NAMESPACE"},
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "API GROUP"},
|
||||
Header{Name: "BINDING"},
|
||||
func (Policy) Header(ns string) Header {
|
||||
h := Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "API GROUP"},
|
||||
HeaderColumn{Name: "BINDING"},
|
||||
}
|
||||
h = append(h, rbacVerbHeader()...)
|
||||
h = append(h, Header{Name: "VALID", Wide: true})
|
||||
h = append(h, HeaderColumn{Name: "VALID", Wide: true})
|
||||
|
||||
return h
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,23 +33,23 @@ type PortForward struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (PortForward) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
return func(ns string, _ Header, re RowEvent) tcell.Color {
|
||||
return tcell.ColorSkyblue
|
||||
}
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (PortForward) Header(ns string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAMESPACE"},
|
||||
Header{Name: "POD"},
|
||||
Header{Name: "CONTAINER"},
|
||||
Header{Name: "PORTS"},
|
||||
Header{Name: "URL"},
|
||||
Header{Name: "C"},
|
||||
Header{Name: "N"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
func (PortForward) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "CONTAINER"},
|
||||
HeaderColumn{Name: "PORTS"},
|
||||
HeaderColumn{Name: "URL"},
|
||||
HeaderColumn{Name: "C"},
|
||||
HeaderColumn{Name: "N"},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,42 +17,45 @@ type PersistentVolume struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (p PersistentVolume) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, re)
|
||||
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
|
||||
if !Happy(ns, re.Row) {
|
||||
return func(ns string, h Header, re RowEvent) tcell.Color {
|
||||
if !Happy(ns, h, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
switch strings.TrimSpace(re.Row.Fields[4]) {
|
||||
case "Bound":
|
||||
c = StdColor
|
||||
case "Available":
|
||||
c = tcell.ColorYellow
|
||||
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||
return DefaultColorer(ns, h, re)
|
||||
}
|
||||
|
||||
return c
|
||||
statusCol := h.IndexOf("STATUS", true)
|
||||
if statusCol == -1 {
|
||||
return DefaultColorer(ns, h, re)
|
||||
}
|
||||
switch strings.TrimSpace(re.Row.Fields[statusCol]) {
|
||||
case "Bound":
|
||||
return StdColor
|
||||
case "Available":
|
||||
return tcell.ColorYellow
|
||||
}
|
||||
|
||||
return DefaultColorer(ns, h, re)
|
||||
}
|
||||
}
|
||||
|
||||
// Header returns a header rbw.
|
||||
func (PersistentVolume) Header(string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "CAPACITY"},
|
||||
Header{Name: "ACCESS MODES"},
|
||||
Header{Name: "RECLAIM POLICY"},
|
||||
Header{Name: "STATUS"},
|
||||
Header{Name: "CLAIM"},
|
||||
Header{Name: "STORAGECLASS"},
|
||||
Header{Name: "REASON"},
|
||||
Header{Name: "VOLUMEMODE", Wide: true},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
func (PersistentVolume) Header(string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "CAPACITY"},
|
||||
HeaderColumn{Name: "ACCESS MODES"},
|
||||
HeaderColumn{Name: "RECLAIM POLICY"},
|
||||
HeaderColumn{Name: "STATUS"},
|
||||
HeaderColumn{Name: "CLAIM"},
|
||||
HeaderColumn{Name: "STORAGECLASS"},
|
||||
HeaderColumn{Name: "REASON"},
|
||||
HeaderColumn{Name: "VOLUMEMODE", Wide: true},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/gdamore/tcell"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
|
@ -15,38 +14,23 @@ type PersistentVolumeClaim struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (p PersistentVolumeClaim) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, re)
|
||||
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
if !Happy(ns, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
return DefaultColorer
|
||||
}
|
||||
|
||||
// Header returns a header rbw.
|
||||
func (PersistentVolumeClaim) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
func (PersistentVolumeClaim) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "STATUS"},
|
||||
HeaderColumn{Name: "VOLUME"},
|
||||
HeaderColumn{Name: "CAPACITY"},
|
||||
HeaderColumn{Name: "ACCESS MODES"},
|
||||
HeaderColumn{Name: "STORAGECLASS"},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "STATUS"},
|
||||
Header{Name: "VOLUME"},
|
||||
Header{Name: "CAPACITY"},
|
||||
Header{Name: "ACCESS MODES"},
|
||||
Header{Name: "STORAGECLASS"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
|
|
@ -81,11 +65,8 @@ func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
|
||||
r.ID = client.MetaFQN(pvc.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(p.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, pvc.Namespace)
|
||||
}
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
pvc.Namespace,
|
||||
pvc.Name,
|
||||
string(phase),
|
||||
pvc.Spec.VolumeName,
|
||||
|
|
@ -95,7 +76,7 @@ func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *Row) error {
|
|||
mapToStr(pvc.Labels),
|
||||
asStatus(p.diagnose(string(phase))),
|
||||
toAge(pvc.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,38 +34,38 @@ type Rbac struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Rbac) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
return func(_ string, _ Header, _re RowEvent) tcell.Color {
|
||||
return tcell.ColorMediumSpringGreen
|
||||
}
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Rbac) Header(ns string) HeaderRow {
|
||||
h := HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "API GROUP"},
|
||||
}
|
||||
|
||||
func (Rbac) Header(ns string) Header {
|
||||
h := make(Header, 0, 10)
|
||||
h = append(h,
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "APIGROUP"},
|
||||
)
|
||||
h = append(h, rbacVerbHeader()...)
|
||||
h = append(h, Header{Name: "VALID", Wide: true})
|
||||
|
||||
return h
|
||||
return append(h, HeaderColumn{Name: "VALID", Wide: true})
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
func (Rbac) Render(o interface{}, gvr string, r *Row) error {
|
||||
func (r Rbac) Render(o interface{}, ns string, ro *Row) error {
|
||||
p, ok := o.(PolicyRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("expecting RuleRes but got %T", o)
|
||||
}
|
||||
|
||||
r.ID = p.Resource
|
||||
r.Fields = append(r.Fields,
|
||||
ro.ID = p.Resource
|
||||
ro.Fields = make(Fields, 0, len(r.Header(ns)))
|
||||
ro.Fields = append(ro.Fields,
|
||||
cleanseResource(p.Resource),
|
||||
p.Group,
|
||||
)
|
||||
r.Fields = append(r.Fields, asVerbs(p.Verbs)...)
|
||||
r.Fields = append(r.Fields, "")
|
||||
ro.Fields = append(ro.Fields, asVerbs(p.Verbs)...)
|
||||
ro.Fields = append(ro.Fields, "")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,17 +18,17 @@ func (Role) ColorerFunc() ColorerFunc {
|
|||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Role) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
func (Role) Header(ns string) Header {
|
||||
var h Header
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
h = append(h, HeaderColumn{Name: "NAMESPACE"})
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,20 +19,20 @@ func (RoleBinding) ColorerFunc() ColorerFunc {
|
|||
}
|
||||
|
||||
// Header returns a header rbw.
|
||||
func (RoleBinding) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
func (RoleBinding) Header(ns string) Header {
|
||||
var h Header
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
h = append(h, HeaderColumn{Name: "NAMESPACE"})
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "ROLE"},
|
||||
Header{Name: "KIND"},
|
||||
Header{Name: "SUBJECTS"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "ROLE"},
|
||||
HeaderColumn{Name: "KIND"},
|
||||
HeaderColumn{Name: "SUBJECTS"},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
|
|
@ -10,6 +11,30 @@ import (
|
|||
// Fields represents a collection of row fields.
|
||||
type Fields []string
|
||||
|
||||
// Customize returns a subset of fields.
|
||||
func (f Fields) Customize(cols []int, out Fields) {
|
||||
for i, c := range cols {
|
||||
if c < 0 {
|
||||
out[i] = "<TOAST!>"
|
||||
continue
|
||||
}
|
||||
if c < len(f) {
|
||||
out[i] = f[c]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Diff returns true if fields differ or false otherwise.
|
||||
func (f Fields) Diff(ff Fields, ageCol int) bool {
|
||||
if ageCol < 0 {
|
||||
return !reflect.DeepEqual(f[:len(f)-1], ff[:len(ff)-1])
|
||||
}
|
||||
if !reflect.DeepEqual(f[:ageCol], ff[:ageCol]) {
|
||||
return true
|
||||
}
|
||||
return !reflect.DeepEqual(f[ageCol+1:], ff[ageCol+1:])
|
||||
}
|
||||
|
||||
// Clone returns a copy of the fields.
|
||||
func (f Fields) Clone() Fields {
|
||||
cp := make(Fields, len(f))
|
||||
|
|
@ -27,8 +52,25 @@ type Row struct {
|
|||
}
|
||||
|
||||
// NewRow returns a new row with initialized fields.
|
||||
func NewRow(cols int) Row {
|
||||
return Row{Fields: make([]string, cols)}
|
||||
func NewRow(size int) Row {
|
||||
return Row{Fields: make([]string, size)}
|
||||
}
|
||||
|
||||
// Customize returns a row subset based on given col indices.
|
||||
func (r Row) Customize(cols []int) Row {
|
||||
out := NewRow(len(cols))
|
||||
r.Fields.Customize(cols, out.Fields)
|
||||
out.ID = r.ID
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Diff returns true if row differ or false otherwise.
|
||||
func (r Row) Diff(ro Row, ageCol int) bool {
|
||||
if r.ID != ro.ID {
|
||||
return true
|
||||
}
|
||||
return r.Fields.Diff(ro.Fields, ageCol)
|
||||
}
|
||||
|
||||
// Clone copies a row.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package render
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
|
|
@ -63,17 +62,30 @@ func (r RowEvent) Clone() RowEvent {
|
|||
}
|
||||
}
|
||||
|
||||
// Customize returns a new subset based on the given column indices.
|
||||
func (r RowEvent) Customize(cols []int) RowEvent {
|
||||
delta := r.Deltas
|
||||
if !r.Deltas.IsBlank() {
|
||||
delta = make(DeltaRow, len(cols))
|
||||
r.Deltas.Customize(cols, delta)
|
||||
}
|
||||
|
||||
return RowEvent{
|
||||
Kind: r.Kind,
|
||||
Deltas: delta,
|
||||
Row: r.Row.Customize(cols),
|
||||
}
|
||||
}
|
||||
|
||||
// Diff returns true if the row changed.
|
||||
func (r RowEvent) Diff(re RowEvent) bool {
|
||||
func (r RowEvent) Diff(re RowEvent, ageCol int) bool {
|
||||
if r.Kind != re.Kind {
|
||||
return true
|
||||
}
|
||||
if !reflect.DeepEqual(r.Deltas, re.Deltas) {
|
||||
if r.Deltas.Diff(re.Deltas, ageCol) {
|
||||
return true
|
||||
}
|
||||
|
||||
// BOZO!! Canned?? Skip age colum
|
||||
return !reflect.DeepEqual(r.Row.Fields[:len(r.Row.Fields)-1], re.Row.Fields[:len(re.Row.Fields)-1])
|
||||
return r.Row.Diff(re.Row, ageCol)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
|
@ -81,14 +93,22 @@ func (r RowEvent) Diff(re RowEvent) bool {
|
|||
// RowEvents a collection of row events.
|
||||
type RowEvents []RowEvent
|
||||
|
||||
func (r RowEvents) Customize(cols []int) RowEvents {
|
||||
ee := make(RowEvents, 0, len(cols))
|
||||
for _, re := range r {
|
||||
ee = append(ee, re.Customize(cols))
|
||||
}
|
||||
return ee
|
||||
}
|
||||
|
||||
// Diff returns true if the event changed.
|
||||
func (rr RowEvents) Diff(r RowEvents) bool {
|
||||
if len(rr) != len(r) {
|
||||
func (r RowEvents) Diff(re RowEvents, ageCol int) bool {
|
||||
if len(r) != len(re) {
|
||||
return true
|
||||
}
|
||||
|
||||
for i := range rr {
|
||||
if rr[i].Diff(r[i]) {
|
||||
for i := range r {
|
||||
if r[i].Diff(re[i], ageCol) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -97,43 +117,43 @@ func (rr RowEvents) Diff(r RowEvents) bool {
|
|||
}
|
||||
|
||||
// Clone returns a rowevents deep copy.
|
||||
func (rr RowEvents) Clone() RowEvents {
|
||||
res := make(RowEvents, len(rr))
|
||||
for i, r := range rr {
|
||||
res[i] = r.Clone()
|
||||
func (r RowEvents) Clone() RowEvents {
|
||||
res := make(RowEvents, len(r))
|
||||
for i, re := range r {
|
||||
res[i] = re.Clone()
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// Upsert add or update a row if it exists.
|
||||
func (rr RowEvents) Upsert(e RowEvent) RowEvents {
|
||||
if idx, ok := rr.FindIndex(e.Row.ID); ok {
|
||||
rr[idx] = e
|
||||
func (r RowEvents) Upsert(re RowEvent) RowEvents {
|
||||
if idx, ok := r.FindIndex(re.Row.ID); ok {
|
||||
r[idx] = re
|
||||
} else {
|
||||
rr = append(rr, e)
|
||||
r = append(r, re)
|
||||
}
|
||||
return rr
|
||||
return r
|
||||
}
|
||||
|
||||
// Delete removes an element by id.
|
||||
func (rr RowEvents) Delete(id string) RowEvents {
|
||||
victim, ok := rr.FindIndex(id)
|
||||
func (r RowEvents) Delete(id string) RowEvents {
|
||||
victim, ok := r.FindIndex(id)
|
||||
if !ok {
|
||||
return rr
|
||||
return r
|
||||
}
|
||||
return append(rr[0:victim], rr[victim+1:]...)
|
||||
return append(r[0:victim], r[victim+1:]...)
|
||||
}
|
||||
|
||||
// Clear delete all row events
|
||||
func (rr RowEvents) Clear() RowEvents {
|
||||
func (r RowEvents) Clear() RowEvents {
|
||||
return RowEvents{}
|
||||
}
|
||||
|
||||
// FindIndex locates a row index by id. Returns false is not found.
|
||||
func (rr RowEvents) FindIndex(id string) (int, bool) {
|
||||
for i, e := range rr {
|
||||
if e.Row.ID == id {
|
||||
func (r RowEvents) FindIndex(id string) (int, bool) {
|
||||
for i, re := range r {
|
||||
if re.Row.ID == id {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
|
|
@ -141,43 +161,36 @@ func (rr RowEvents) FindIndex(id string) (int, bool) {
|
|||
return 0, false
|
||||
}
|
||||
|
||||
func (rr RowEvents) isAgeCol(col int) bool {
|
||||
var age bool
|
||||
if len(rr) == 0 {
|
||||
return age
|
||||
}
|
||||
return col == len(rr[0].Row.Fields)-1
|
||||
}
|
||||
|
||||
// Sort rows based on column index and order.
|
||||
func (rr RowEvents) Sort(ns string, col int, asc bool) {
|
||||
t := RowEventSorter{NS: ns, Events: rr, Index: col, Asc: asc}
|
||||
func (r RowEvents) Sort(ns string, sortCol int, ageCol bool, asc bool) {
|
||||
t := RowEventSorter{NS: ns, Events: r, Index: sortCol, Asc: asc}
|
||||
sort.Sort(t)
|
||||
|
||||
ageCol := rr.isAgeCol(col)
|
||||
gg, kk := map[string][]string{}, make(StringSet, 0, len(rr))
|
||||
for _, r := range rr {
|
||||
g := r.Row.Fields[col]
|
||||
gg, kk := map[string][]string{}, make(StringSet, 0, len(r))
|
||||
for _, re := range r {
|
||||
g := re.Row.Fields[sortCol]
|
||||
if ageCol {
|
||||
g = toAgeDuration(g)
|
||||
}
|
||||
kk = kk.Add(g)
|
||||
if ss, ok := gg[g]; ok {
|
||||
gg[g] = append(ss, r.Row.ID)
|
||||
gg[g] = append(ss, re.Row.ID)
|
||||
} else {
|
||||
gg[g] = []string{r.Row.ID}
|
||||
gg[g] = []string{re.Row.ID}
|
||||
}
|
||||
}
|
||||
|
||||
ids := make([]string, 0, len(rr))
|
||||
ids := make([]string, 0, len(r))
|
||||
for _, k := range kk {
|
||||
sort.StringSlice(gg[k]).Sort()
|
||||
ids = append(ids, gg[k]...)
|
||||
}
|
||||
s := IdSorter{Ids: ids, Events: rr}
|
||||
s := IdSorter{Ids: ids, Events: r}
|
||||
sort.Sort(s)
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
||||
func toAgeDuration(dur string) string {
|
||||
d, err := time.ParseDuration(dur)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,354 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRowEventCustomize(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
re1, e render.RowEvent
|
||||
cols []int
|
||||
}{
|
||||
"empty": {
|
||||
re1: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
|
||||
},
|
||||
e: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{}},
|
||||
},
|
||||
},
|
||||
"full": {
|
||||
re1: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
|
||||
},
|
||||
e: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
|
||||
},
|
||||
cols: []int{0, 1, 2},
|
||||
},
|
||||
"deltas": {
|
||||
re1: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
|
||||
Deltas: render.DeltaRow{"a", "b", "c"},
|
||||
},
|
||||
e: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
|
||||
Deltas: render.DeltaRow{"a", "b", "c"},
|
||||
},
|
||||
cols: []int{0, 1, 2},
|
||||
},
|
||||
"deltas-skip": {
|
||||
re1: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
|
||||
Deltas: render.DeltaRow{"a", "b", "c"},
|
||||
},
|
||||
e: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"3", "1"}},
|
||||
Deltas: render.DeltaRow{"c", "a"},
|
||||
},
|
||||
cols: []int{2, 0},
|
||||
},
|
||||
"reverse": {
|
||||
re1: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
|
||||
},
|
||||
e: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"3", "2", "1"}},
|
||||
},
|
||||
cols: []int{2, 1, 0},
|
||||
},
|
||||
"skip": {
|
||||
re1: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
|
||||
},
|
||||
e: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"3", "1"}},
|
||||
},
|
||||
cols: []int{2, 0},
|
||||
},
|
||||
"miss": {
|
||||
re1: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
|
||||
},
|
||||
e: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"3", "", "1"}},
|
||||
},
|
||||
cols: []int{2, 10, 0},
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, u.re1.Customize(u.cols))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRowEventDiff(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
re1, re2 render.RowEvent
|
||||
e bool
|
||||
}{
|
||||
"same": {
|
||||
re1: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
|
||||
},
|
||||
re2: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
|
||||
},
|
||||
},
|
||||
"diff-kind": {
|
||||
re1: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
|
||||
},
|
||||
re2: render.RowEvent{
|
||||
Kind: render.EventDelete,
|
||||
Row: render.Row{ID: "B", Fields: render.Fields{"1", "2", "3"}},
|
||||
},
|
||||
e: true,
|
||||
},
|
||||
"diff-delta": {
|
||||
re1: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
|
||||
Deltas: render.DeltaRow{"1", "2", "3"},
|
||||
},
|
||||
re2: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
|
||||
Deltas: render.DeltaRow{"10", "2", "3"},
|
||||
},
|
||||
e: true,
|
||||
},
|
||||
"diff-id": {
|
||||
re1: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
|
||||
},
|
||||
re2: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "B", Fields: render.Fields{"1", "2", "3"}},
|
||||
},
|
||||
e: true,
|
||||
},
|
||||
"diff-field": {
|
||||
re1: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
|
||||
},
|
||||
re2: render.RowEvent{
|
||||
Kind: render.EventAdd,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"10", "2", "3"}},
|
||||
},
|
||||
e: true,
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, u.re1.Diff(u.re2, -1))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRowEventsDiff(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
re1, re2 render.RowEvents
|
||||
ageCol int
|
||||
e bool
|
||||
}{
|
||||
"same": {
|
||||
re1: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
re2: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
ageCol: -1,
|
||||
},
|
||||
"diff-len": {
|
||||
re1: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
re2: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
ageCol: -1,
|
||||
e: true,
|
||||
},
|
||||
"diff-id": {
|
||||
re1: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
re2: render.RowEvents{
|
||||
{Row: render.Row{ID: "D", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
ageCol: -1,
|
||||
e: true,
|
||||
},
|
||||
"diff-order": {
|
||||
re1: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
re2: render.RowEvents{
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
ageCol: -1,
|
||||
e: true,
|
||||
},
|
||||
"diff-withAge": {
|
||||
re1: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
re2: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "13"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
ageCol: 1,
|
||||
e: true,
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, u.re1.Diff(u.re2, u.ageCol))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRowEventsUpsert(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
ee, e render.RowEvents
|
||||
re render.RowEvent
|
||||
}{
|
||||
"add": {
|
||||
ee: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
re: render.RowEvent{
|
||||
Row: render.Row{ID: "D", Fields: render.Fields{"f1", "f2", "f3"}},
|
||||
},
|
||||
e: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
{Row: render.Row{ID: "D", Fields: render.Fields{"f1", "f2", "f3"}}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, u.ee.Upsert(u.re))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRowEventsCustomize(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
re, e render.RowEvents
|
||||
cols []int
|
||||
}{
|
||||
"same": {
|
||||
re: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
cols: []int{0, 1, 2},
|
||||
e: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
},
|
||||
"reverse": {
|
||||
re: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
cols: []int{2, 1, 0},
|
||||
e: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"3", "2", "1"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"3", "2", "0"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"3", "2", "10"}}},
|
||||
},
|
||||
},
|
||||
"skip": {
|
||||
re: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
cols: []int{1, 0},
|
||||
e: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"2", "1"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"2", "0"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"2", "10"}}},
|
||||
},
|
||||
},
|
||||
"missing": {
|
||||
re: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
cols: []int{1, 0, 4},
|
||||
e: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"2", "1", ""}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"2", "0", ""}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"2", "10", ""}}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, u.re.Customize(u.cols))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRowEventsDelete(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
re render.RowEvents
|
||||
|
|
@ -60,7 +408,7 @@ func TestRowEventsDelete(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSort(t *testing.T) {
|
||||
func TestRowEventsSort(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
re render.RowEvents
|
||||
col int
|
||||
|
|
@ -106,12 +454,37 @@ func TestSort(t *testing.T) {
|
|||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
u.re.Sort("", u.col, u.asc)
|
||||
u.re.Sort("", u.col, false, u.asc)
|
||||
assert.Equal(t, u.e, u.re)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRowEventsClone(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
r render.RowEvents
|
||||
}{
|
||||
"empty": {
|
||||
r: render.RowEvents{},
|
||||
},
|
||||
"full": {
|
||||
r: makeRowEvents(),
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
c := u.r.Clone()
|
||||
assert.Equal(t, len(u.r), len(c))
|
||||
if len(u.r) > 0 {
|
||||
u.r[0].Row.Fields[0] = "blee"
|
||||
assert.Equal(t, "A", c[0].Row.Fields[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultColorer(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
k render.ResEvent
|
||||
|
|
@ -123,10 +496,29 @@ func TestDefaultColorer(t *testing.T) {
|
|||
"std": {100, render.StdColor},
|
||||
}
|
||||
|
||||
h := render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, render.DefaultColorer("", render.RowEvent{}))
|
||||
assert.Equal(t, u.e, render.DefaultColorer("", h, render.RowEvent{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
||||
func makeRowEvents() render.RowEvents {
|
||||
return render.RowEvents{
|
||||
{Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3"}}},
|
||||
{Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3"}}},
|
||||
{Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3"}}},
|
||||
{Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3"}}},
|
||||
{Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3"}}},
|
||||
{Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3"}}},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,85 +0,0 @@
|
|||
package render
|
||||
|
||||
import "reflect"
|
||||
|
||||
const ageCol = "AGE"
|
||||
|
||||
// Header represent a table header
|
||||
type Header struct {
|
||||
Name string
|
||||
Align int
|
||||
Decorator DecoratorFunc
|
||||
Hide bool
|
||||
Wide bool
|
||||
}
|
||||
|
||||
// Clone copies a header.
|
||||
func (h Header) Clone() Header {
|
||||
return h
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// HeaderRow represents a table header.
|
||||
type HeaderRow []Header
|
||||
|
||||
// Clone duplicates a header.
|
||||
func (hh HeaderRow) Clone() HeaderRow {
|
||||
h := make(HeaderRow, len(hh))
|
||||
for i, v := range hh {
|
||||
h[i] = v.Clone()
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// Clear clears out the header row.
|
||||
func (hh HeaderRow) Clear() HeaderRow {
|
||||
return HeaderRow{}
|
||||
}
|
||||
|
||||
// Diff returns true if the header changed.
|
||||
func (hh HeaderRow) Diff(h HeaderRow) bool {
|
||||
if len(hh) != len(h) {
|
||||
return true
|
||||
}
|
||||
return !reflect.DeepEqual(hh.Columns(), h.Columns())
|
||||
}
|
||||
|
||||
// Columns return header as a collection of strings.
|
||||
func (hh HeaderRow) Columns() []string {
|
||||
cc := make([]string, len(hh))
|
||||
for i, c := range hh {
|
||||
cc[i] = c.Name
|
||||
}
|
||||
|
||||
return cc
|
||||
}
|
||||
|
||||
// HasAge returns true if table has an age column.
|
||||
func (hh HeaderRow) HasAge() bool {
|
||||
return hh.IndexOf(ageCol) != -1
|
||||
}
|
||||
|
||||
// AgeCol checks if given column index is the age column.
|
||||
func (hh HeaderRow) AgeCol(col int) bool {
|
||||
if !hh.HasAge() {
|
||||
return false
|
||||
}
|
||||
return col == len(hh)-1
|
||||
}
|
||||
|
||||
// ValidColIndex returns the valid col index or -1 if none.
|
||||
func (hh HeaderRow) ValidColIndex() int {
|
||||
return hh.IndexOf("VALID")
|
||||
}
|
||||
|
||||
// IndexOf returns the col index or -1 if none.
|
||||
func (hh HeaderRow) IndexOf(c string) int {
|
||||
for i, h := range hh {
|
||||
if h.Name == c {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
|
@ -9,6 +9,54 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func BenchmarkRowCustomize(b *testing.B) {
|
||||
row := render.Row{ID: "fred", Fields: render.Fields{"f1", "f2", "f3"}}
|
||||
cols := []int{0, 1, 2}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
_ = row.Customize(cols)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldCustomize(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
fields render.Fields
|
||||
cols []int
|
||||
e render.Fields
|
||||
}{
|
||||
"empty": {
|
||||
fields: render.Fields{},
|
||||
cols: []int{0, 1, 2},
|
||||
e: render.Fields{"", "", ""},
|
||||
},
|
||||
"no-cols": {
|
||||
fields: render.Fields{"f1", "f2", "f3"},
|
||||
cols: []int{},
|
||||
e: render.Fields{},
|
||||
},
|
||||
"reverse": {
|
||||
fields: render.Fields{"f1", "f2", "f3"},
|
||||
cols: []int{1, 0},
|
||||
e: render.Fields{"f2", "f1"},
|
||||
},
|
||||
"missing": {
|
||||
fields: render.Fields{"f1", "f2", "f3"},
|
||||
cols: []int{10, 0},
|
||||
e: render.Fields{"", "f1"},
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
ff := make(render.Fields, len(u.cols))
|
||||
u.fields.Customize(u.cols, ff)
|
||||
assert.Equal(t, u.e, ff)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldClone(t *testing.T) {
|
||||
f := render.Fields{"a", "b", "c"}
|
||||
f1 := f.Clone()
|
||||
|
|
@ -17,6 +65,38 @@ func TestFieldClone(t *testing.T) {
|
|||
assert.NotEqual(t, fmt.Sprintf("%p", f), fmt.Sprintf("%p", f1))
|
||||
}
|
||||
|
||||
func TestRowCustomize(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
row render.Row
|
||||
cols []int
|
||||
e render.Row
|
||||
}{
|
||||
"empty": {
|
||||
row: render.Row{},
|
||||
cols: []int{0, 1, 2},
|
||||
e: render.Row{ID: "", Fields: render.Fields{"", "", ""}},
|
||||
},
|
||||
"no-cols-no-data": {
|
||||
row: render.Row{},
|
||||
cols: []int{},
|
||||
e: render.Row{ID: "", Fields: render.Fields{}},
|
||||
},
|
||||
"no-cols-data": {
|
||||
row: render.Row{ID: "fred", Fields: render.Fields{"f1", "f2", "f3"}},
|
||||
cols: []int{},
|
||||
e: render.Row{ID: "fred", Fields: render.Fields{}},
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
row := u.row.Customize(u.cols)
|
||||
assert.Equal(t, u.e, row)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRowsDelete(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
rows render.Rows
|
||||
|
|
@ -69,10 +149,50 @@ func TestRowsDelete(t *testing.T) {
|
|||
}
|
||||
|
||||
for k := range uu {
|
||||
uc := uu[k]
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
rows := uc.rows.Delete(uc.id)
|
||||
assert.Equal(t, uc.e, rows)
|
||||
rows := u.rows.Delete(u.id)
|
||||
assert.Equal(t, u.e, rows)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRowsUpsert(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
rows render.Rows
|
||||
row render.Row
|
||||
e render.Rows
|
||||
}{
|
||||
"add": {
|
||||
rows: render.Rows{
|
||||
{ID: "a", Fields: []string{"blee", "duh"}},
|
||||
{ID: "b", Fields: []string{"albert", "blee"}},
|
||||
},
|
||||
row: render.Row{ID: "c", Fields: []string{"f1", "f2"}},
|
||||
e: render.Rows{
|
||||
{ID: "a", Fields: []string{"blee", "duh"}},
|
||||
{ID: "b", Fields: []string{"albert", "blee"}},
|
||||
{ID: "c", Fields: []string{"f1", "f2"}},
|
||||
},
|
||||
},
|
||||
"update": {
|
||||
rows: render.Rows{
|
||||
{ID: "a", Fields: []string{"blee", "duh"}},
|
||||
{ID: "b", Fields: []string{"albert", "blee"}},
|
||||
},
|
||||
row: render.Row{ID: "a", Fields: []string{"f1", "f2"}},
|
||||
e: render.Rows{
|
||||
{ID: "a", Fields: []string{"f1", "f2"}},
|
||||
{ID: "b", Fields: []string{"albert", "blee"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
rows := u.rows.Upsert(u.row)
|
||||
assert.Equal(t, u.e, rows)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -147,10 +267,10 @@ func TestRowsSortText(t *testing.T) {
|
|||
}
|
||||
|
||||
for k := range uu {
|
||||
uc := uu[k]
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
uc.rows.Sort(uc.col, uc.asc)
|
||||
assert.Equal(t, uc.e, uc.rows)
|
||||
u.rows.Sort(u.col, u.asc)
|
||||
assert.Equal(t, u.e, u.rows)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -188,10 +308,10 @@ func TestRowsSortDuration(t *testing.T) {
|
|||
}
|
||||
|
||||
for k := range uu {
|
||||
uc := uu[k]
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
uc.rows.Sort(uc.col, uc.asc)
|
||||
assert.Equal(t, uc.e, uc.rows)
|
||||
u.rows.Sort(u.col, u.asc)
|
||||
assert.Equal(t, u.e, u.rows)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -230,10 +350,10 @@ func TestRowsSortMetrics(t *testing.T) {
|
|||
}
|
||||
|
||||
for k := range uu {
|
||||
uc := uu[k]
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
uc.rows.Sort(uc.col, uc.asc)
|
||||
assert.Equal(t, uc.e, uc.rows)
|
||||
u.rows.Sort(u.col, u.asc)
|
||||
assert.Equal(t, u.e, u.rows)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
|
@ -17,36 +16,21 @@ type ReplicaSet struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (r ReplicaSet) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, re)
|
||||
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
|
||||
if !Happy(ns, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return StdColor
|
||||
}
|
||||
return DefaultColorer
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (ReplicaSet) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
func (ReplicaSet) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "DESIRED", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "CURRENT", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "READY", Align: tview.AlignRight},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "DESIRED", Align: tview.AlignRight},
|
||||
Header{Name: "CURRENT", Align: tview.AlignRight},
|
||||
Header{Name: "READY", Align: tview.AlignRight},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
|
|
@ -62,11 +46,8 @@ func (r ReplicaSet) Render(o interface{}, ns string, row *Row) error {
|
|||
}
|
||||
|
||||
row.ID = client.MetaFQN(rs.ObjectMeta)
|
||||
row.Fields = make(Fields, 0, len(r.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
row.Fields = append(row.Fields, rs.Namespace)
|
||||
}
|
||||
row.Fields = append(row.Fields,
|
||||
row.Fields = Fields{
|
||||
rs.Namespace,
|
||||
rs.Name,
|
||||
strconv.Itoa(int(*rs.Spec.Replicas)),
|
||||
strconv.Itoa(int(rs.Status.Replicas)),
|
||||
|
|
@ -74,7 +55,7 @@ func (r ReplicaSet) Render(o interface{}, ns string, row *Row) error {
|
|||
mapToStr(rs.Labels),
|
||||
asStatus(r.diagnose(rs)),
|
||||
toAge(rs.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,19 +19,15 @@ func (ServiceAccount) ColorerFunc() ColorerFunc {
|
|||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (ServiceAccount) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
func (ServiceAccount) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "SECRET"},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "SECRET"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
|
|
@ -47,17 +43,14 @@ func (s ServiceAccount) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
|
||||
r.ID = client.MetaFQN(sa.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(s.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, sa.Namespace)
|
||||
}
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
sa.Namespace,
|
||||
sa.Name,
|
||||
strconv.Itoa(len(sa.Secrets)),
|
||||
mapToStr(sa.Labels),
|
||||
"",
|
||||
toAge(sa.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ func (StorageClass) ColorerFunc() ColorerFunc {
|
|||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (StorageClass) Header(ns string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "PROVISIONER"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
func (StorageClass) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "PROVISIONER"},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ type ScreenDump struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (ScreenDump) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
return func(ns string, _ Header, re RowEvent) tcell.Color {
|
||||
return tcell.ColorNavajoWhite
|
||||
}
|
||||
}
|
||||
|
|
@ -30,12 +30,12 @@ var AgeDecorator = func(a string) string {
|
|||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (ScreenDump) Header(ns string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "DIR"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
func (ScreenDump) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "DIR"},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"strconv"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/gdamore/tcell"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
|
@ -16,37 +15,23 @@ type StatefulSet struct{}
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (s StatefulSet) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, re)
|
||||
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
if !Happy(ns, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return StdColor
|
||||
}
|
||||
return DefaultColorer
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (StatefulSet) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
func (StatefulSet) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "READY"},
|
||||
HeaderColumn{Name: "SELECTOR", Wide: true},
|
||||
HeaderColumn{Name: "SERVICE"},
|
||||
HeaderColumn{Name: "CONTAINERS", Wide: true},
|
||||
HeaderColumn{Name: "IMAGES", Wide: true},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "READY"},
|
||||
Header{Name: "SELECTOR", Wide: true},
|
||||
Header{Name: "SERVICE"},
|
||||
Header{Name: "CONTAINERS", Wide: true},
|
||||
Header{Name: "IMAGES", Wide: true},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
|
|
@ -62,13 +47,10 @@ func (s StatefulSet) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
|
||||
r.ID = client.MetaFQN(sts.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(s.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, sts.Namespace)
|
||||
}
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
sts.Namespace,
|
||||
sts.Name,
|
||||
strconv.Itoa(int(sts.Status.ReadyReplicas))+"/"+strconv.Itoa(int(sts.Status.Replicas)),
|
||||
strconv.Itoa(int(sts.Status.ReadyReplicas)) + "/" + strconv.Itoa(int(sts.Status.Replicas)),
|
||||
asSelector(sts.Spec.Selector),
|
||||
na(sts.Spec.ServiceName),
|
||||
podContainerNames(sts.Spec.Template.Spec, true),
|
||||
|
|
@ -76,7 +58,7 @@ func (s StatefulSet) Render(o interface{}, ns string, r *Row) error {
|
|||
mapToStr(sts.Labels),
|
||||
asStatus(s.diagnose(sts.Status.Replicas, sts.Status.ReadyReplicas)),
|
||||
toAge(sts.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,18 +18,18 @@ func (Subject) Happy(_ string, _ Row) bool {
|
|||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Subject) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
return func(ns string, _ Header, re RowEvent) tcell.Color {
|
||||
return tcell.ColorMediumSpringGreen
|
||||
}
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Subject) Header(ns string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "KIND"},
|
||||
Header{Name: "FIRST LOCATION"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
func (Subject) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "KIND"},
|
||||
HeaderColumn{Name: "FIRST LOCATION"},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,13 +41,12 @@ func (s Subject) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
|
||||
r.ID = res.Name
|
||||
r.Fields = make(Fields, 0, len(s.Header(ns)))
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
res.Name,
|
||||
res.Kind,
|
||||
res.FirstLocation,
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,23 +21,19 @@ func (Service) ColorerFunc() ColorerFunc {
|
|||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Service) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
if client.IsAllNamespaces(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
func (Service) Header(ns string) Header {
|
||||
return Header{
|
||||
HeaderColumn{Name: "NAMESPACE"},
|
||||
HeaderColumn{Name: "NAME"},
|
||||
HeaderColumn{Name: "TYPE"},
|
||||
HeaderColumn{Name: "CLUSTER-IP"},
|
||||
HeaderColumn{Name: "EXTERNAL-IP"},
|
||||
HeaderColumn{Name: "SELECTOR", Wide: true},
|
||||
HeaderColumn{Name: "PORTS", Wide: true},
|
||||
HeaderColumn{Name: "LABELS", Wide: true},
|
||||
HeaderColumn{Name: "VALID", Wide: true},
|
||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "TYPE"},
|
||||
Header{Name: "CLUSTER-IP"},
|
||||
Header{Name: "EXTERNAL-IP"},
|
||||
Header{Name: "SELECTOR", Wide: true},
|
||||
Header{Name: "PORTS", Wide: true},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
|
|
@ -53,21 +49,18 @@ func (s Service) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
|
||||
r.ID = client.MetaFQN(svc.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(s.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
r.Fields = append(r.Fields, svc.Namespace)
|
||||
}
|
||||
r.Fields = append(r.Fields,
|
||||
r.Fields = Fields{
|
||||
svc.Namespace,
|
||||
svc.ObjectMeta.Name,
|
||||
string(svc.Spec.Type),
|
||||
toIP(svc.Spec.ClusterIP),
|
||||
toIPs(svc.Spec.Type, getSvcExtIPS(&svc)),
|
||||
mapToStr(svc.Spec.Selector),
|
||||
toPorts(svc.Spec.Ports),
|
||||
ToPorts(svc.Spec.Ports),
|
||||
mapToStr(svc.Labels),
|
||||
asStatus(s.diagnose()),
|
||||
toAge(svc.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -138,7 +131,7 @@ func toIPs(svcType v1.ServiceType, ips []string) string {
|
|||
return strings.Join(ips, ",")
|
||||
}
|
||||
|
||||
func toPorts(pp []v1.ServicePort) string {
|
||||
func ToPorts(pp []v1.ServicePort) string {
|
||||
ports := make([]string, len(pp))
|
||||
for i, p := range pp {
|
||||
if len(p.Name) > 0 {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package render
|
||||
|
||||
import "github.com/rs/zerolog/log"
|
||||
|
||||
// TableData tracks a K8s resource for tabular display.
|
||||
type TableData struct {
|
||||
Header HeaderRow
|
||||
Header Header
|
||||
RowEvents RowEvents
|
||||
Namespace string
|
||||
}
|
||||
|
|
@ -12,9 +14,22 @@ func NewTableData() *TableData {
|
|||
return &TableData{}
|
||||
}
|
||||
|
||||
func (t *TableData) Customize(cols []string, wide bool) TableData {
|
||||
res := TableData{
|
||||
Namespace: t.Namespace,
|
||||
Header: t.Header.Customize(cols, wide),
|
||||
}
|
||||
ids := make([]int, len(t.Header))
|
||||
t.Header.MapIndices(cols, wide, ids)
|
||||
log.Debug().Msgf("INDICES %#v", ids)
|
||||
res.RowEvents = t.RowEvents.Customize(ids)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// Clear clears out the entire table.
|
||||
func (t *TableData) Clear() {
|
||||
t.Header, t.RowEvents = t.Header.Clear(), t.RowEvents.Clear()
|
||||
t.Header, t.RowEvents = Header{}, RowEvents{}
|
||||
}
|
||||
|
||||
// Clone returns a copy of the table
|
||||
|
|
@ -27,7 +42,7 @@ func (t *TableData) Clone() TableData {
|
|||
}
|
||||
|
||||
// SetHeader sets table header.
|
||||
func (t *TableData) SetHeader(ns string, h HeaderRow) {
|
||||
func (t *TableData) SetHeader(ns string, h Header) {
|
||||
t.Namespace, t.Header = ns, h
|
||||
}
|
||||
|
||||
|
|
@ -85,5 +100,5 @@ func (t *TableData) Diff(table TableData) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
return t.RowEvents.Diff(table.RowEvents)
|
||||
return t.RowEvents.Diff(table.RowEvents, t.Header.IndexOf("AGE", true))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,349 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTableDataCustomize(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
t1 render.TableData
|
||||
cols []string
|
||||
wide bool
|
||||
e render.TableData
|
||||
}{
|
||||
"same": {
|
||||
t1: render.TableData{
|
||||
Namespace: "fred",
|
||||
Header: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
RowEvents: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
},
|
||||
cols: []string{"A", "B", "C"},
|
||||
e: render.TableData{
|
||||
Namespace: "fred",
|
||||
Header: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
RowEvents: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
"wide-col": {
|
||||
t1: render.TableData{
|
||||
Namespace: "fred",
|
||||
Header: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B", Wide: true},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
RowEvents: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
},
|
||||
cols: []string{"A", "B", "C"},
|
||||
e: render.TableData{
|
||||
Namespace: "fred",
|
||||
Header: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B", Wide: false},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
RowEvents: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
"wide": {
|
||||
t1: render.TableData{
|
||||
Namespace: "fred",
|
||||
Header: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B", Wide: true},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
RowEvents: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
},
|
||||
wide: true,
|
||||
cols: []string{"A", "C"},
|
||||
e: render.TableData{
|
||||
Namespace: "fred",
|
||||
Header: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
render.HeaderColumn{Name: "B", Wide: true},
|
||||
},
|
||||
RowEvents: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "3", "2"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "3", "2"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "3", "2"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, u.t1.Customize(u.cols, u.wide))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTableDataDiff(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
t1 render.TableData
|
||||
t2 render.TableData
|
||||
e bool
|
||||
}{
|
||||
"empty": {
|
||||
t1: render.TableData{
|
||||
Namespace: "fred",
|
||||
Header: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
RowEvents: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
},
|
||||
e: true,
|
||||
},
|
||||
"same": {
|
||||
t1: render.TableData{
|
||||
Namespace: "fred",
|
||||
Header: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
RowEvents: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
},
|
||||
t2: render.TableData{
|
||||
Namespace: "fred",
|
||||
Header: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
RowEvents: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
"ns-diff": {
|
||||
t1: render.TableData{
|
||||
Namespace: "fred",
|
||||
Header: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
RowEvents: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
},
|
||||
t2: render.TableData{
|
||||
Namespace: "blee",
|
||||
Header: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
RowEvents: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
},
|
||||
e: true,
|
||||
},
|
||||
"header-diff": {
|
||||
t1: render.TableData{
|
||||
Namespace: "fred",
|
||||
Header: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "D"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
RowEvents: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
},
|
||||
t2: render.TableData{
|
||||
Namespace: "fred",
|
||||
Header: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
RowEvents: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
},
|
||||
e: true,
|
||||
},
|
||||
"row-diff": {
|
||||
t1: render.TableData{
|
||||
Namespace: "fred",
|
||||
Header: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
RowEvents: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
},
|
||||
t2: render.TableData{
|
||||
Namespace: "fred",
|
||||
Header: render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
},
|
||||
RowEvents: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"100", "2", "3"}}},
|
||||
},
|
||||
},
|
||||
e: true,
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, u.t1.Diff(u.t2))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTableDataUpdate(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
re render.RowEvents
|
||||
rr render.Rows
|
||||
e render.RowEvents
|
||||
}{
|
||||
"no-change": {
|
||||
re: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
rr: render.Rows{
|
||||
render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
|
||||
render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}},
|
||||
render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}},
|
||||
},
|
||||
e: render.RowEvents{
|
||||
{Kind: render.EventUnchanged, Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Kind: render.EventUnchanged, Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
},
|
||||
"add": {
|
||||
re: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
rr: render.Rows{
|
||||
render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
|
||||
render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}},
|
||||
render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}},
|
||||
render.Row{ID: "D", Fields: render.Fields{"10", "2", "3"}},
|
||||
},
|
||||
e: render.RowEvents{
|
||||
{Kind: render.EventUnchanged, Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Kind: render.EventUnchanged, Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
{Kind: render.EventAdd, Row: render.Row{ID: "D", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
},
|
||||
"delete": {
|
||||
re: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
rr: render.Rows{
|
||||
render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
|
||||
render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}},
|
||||
},
|
||||
e: render.RowEvents{
|
||||
{Kind: render.EventUnchanged, Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
},
|
||||
"update": {
|
||||
re: render.RowEvents{
|
||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
rr: render.Rows{
|
||||
render.Row{ID: "A", Fields: render.Fields{"10", "2", "3"}},
|
||||
render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}},
|
||||
render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}},
|
||||
},
|
||||
e: render.RowEvents{
|
||||
{
|
||||
Kind: render.EventUpdate,
|
||||
Row: render.Row{ID: "A", Fields: render.Fields{"10", "2", "3"}},
|
||||
Deltas: render.DeltaRow{"1", "", ""},
|
||||
},
|
||||
{Kind: render.EventUnchanged, Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
|
||||
{Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var table render.TableData
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
table.RowEvents = u.re
|
||||
table.Update(u.rr)
|
||||
assert.Equal(t, u.e, table.RowEvents)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTableDataDelete(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
re render.RowEvents
|
||||
|
|
|
|||
|
|
@ -20,10 +20,11 @@ type synchronizer interface {
|
|||
|
||||
// Configurator represents an application configurationa.
|
||||
type Configurator struct {
|
||||
skinFile string
|
||||
Config *config.Config
|
||||
Styles *config.Styles
|
||||
BenchFile string
|
||||
Config *config.Config
|
||||
Styles *config.Styles
|
||||
CustomView *config.CustomView
|
||||
BenchFile string
|
||||
skinFile string
|
||||
}
|
||||
|
||||
// HasSkin returns true if a skin file was located.
|
||||
|
|
@ -31,8 +32,55 @@ func (c *Configurator) HasSkin() bool {
|
|||
return c.skinFile != ""
|
||||
}
|
||||
|
||||
// StylesUpdater watches for skin file changes.
|
||||
func (c *Configurator) StylesUpdater(ctx context.Context, s synchronizer) error {
|
||||
// CustomViewsWatcher watches for view config file changes.
|
||||
func (c *Configurator) CustomViewsWatcher(ctx context.Context, s synchronizer) error {
|
||||
w, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case evt := <-w.Events:
|
||||
_ = evt
|
||||
s.QueueUpdateDraw(func() {
|
||||
c.RefreshCustomViews()
|
||||
})
|
||||
case err := <-w.Errors:
|
||||
log.Info().Err(err).Msg("CustomView watcher failed")
|
||||
return
|
||||
case <-ctx.Done():
|
||||
log.Debug().Msgf("CustomViewWatcher Done `%s!!", config.K9sViewConfigFile)
|
||||
if err := w.Close(); err != nil {
|
||||
log.Error().Err(err).Msg("Closing CustomView watcher")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
log.Debug().Msgf("CustomView watching `%s", config.K9sViewConfigFile)
|
||||
c.RefreshCustomViews()
|
||||
return w.Add(config.K9sViewConfigFile)
|
||||
}
|
||||
|
||||
// RefreshCustomView load view configuration changes.
|
||||
func (c *Configurator) RefreshCustomViews() {
|
||||
if c.CustomView == nil {
|
||||
c.CustomView = config.NewCustomView()
|
||||
} else {
|
||||
c.CustomView.Reset()
|
||||
}
|
||||
|
||||
if err := c.CustomView.Load(config.K9sViewConfigFile); err != nil {
|
||||
log.Debug().Msgf("No view custom configuration file found -- %s", config.K9sViewConfigFile)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// StylesWatcher watches for skin file changes.
|
||||
func (c *Configurator) StylesWatcher(ctx context.Context, s synchronizer) error {
|
||||
if !c.HasSkin() {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -56,7 +104,7 @@ func (c *Configurator) StylesUpdater(ctx context.Context, s synchronizer) error
|
|||
case <-ctx.Done():
|
||||
log.Debug().Msgf("SkinWatcher Done `%s!!", c.skinFile)
|
||||
if err := w.Close(); err != nil {
|
||||
log.Error().Err(err).Msg("Closing watcher")
|
||||
log.Error().Err(err).Msg("Closing Skin watcher")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ import (
|
|||
type MaxyPad []int
|
||||
|
||||
// ComputeMaxColumns figures out column max size and necessary padding.
|
||||
func ComputeMaxColumns(pads MaxyPad, sortCol int, header render.HeaderRow, ee render.RowEvents) {
|
||||
func ComputeMaxColumns(pads MaxyPad, sortColName string, header render.Header, ee render.RowEvents) {
|
||||
const colPadding = 1
|
||||
|
||||
for index, h := range header {
|
||||
pads[index] = len(h.Name)
|
||||
if index == sortCol {
|
||||
if h.Name == sortColName {
|
||||
pads[index] = len(h.Name) + 2
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ func ComputeMaxColumns(pads MaxyPad, sortCol int, header render.HeaderRow, ee re
|
|||
var row int
|
||||
for _, e := range ee {
|
||||
for index, field := range e.Row.Fields {
|
||||
if header.AgeCol(index) {
|
||||
if header.IsAgeCol(index) {
|
||||
field = toAgeHuman(field)
|
||||
}
|
||||
width := len(field) + colPadding
|
||||
|
|
@ -53,11 +53,9 @@ func Pad(s string, width int) string {
|
|||
if len(s) == width {
|
||||
return s
|
||||
}
|
||||
|
||||
if len(s) > width {
|
||||
return render.Truncate(s, width)
|
||||
}
|
||||
|
||||
return s + strings.Repeat(" ", width-len(s))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ import (
|
|||
func TestMaxColumn(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
t render.TableData
|
||||
s int
|
||||
s string
|
||||
e MaxyPad
|
||||
}{
|
||||
"ascii col 0": {
|
||||
render.TableData{
|
||||
Header: render.HeaderRow{render.Header{Name: "A"}, render.Header{Name: "B"}},
|
||||
Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}},
|
||||
RowEvents: render.RowEvents{
|
||||
render.RowEvent{
|
||||
Row: render.Row{
|
||||
|
|
@ -29,12 +29,12 @@ func TestMaxColumn(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
0,
|
||||
"A",
|
||||
MaxyPad{6, 6},
|
||||
},
|
||||
"ascii col 1": {
|
||||
render.TableData{
|
||||
Header: render.HeaderRow{render.Header{Name: "A"}, render.Header{Name: "B"}},
|
||||
Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}},
|
||||
RowEvents: render.RowEvents{
|
||||
render.RowEvent{
|
||||
Row: render.Row{
|
||||
|
|
@ -48,12 +48,12 @@ func TestMaxColumn(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
1,
|
||||
"B",
|
||||
MaxyPad{6, 6},
|
||||
},
|
||||
"non_ascii": {
|
||||
render.TableData{
|
||||
Header: render.HeaderRow{render.Header{Name: "A"}, render.Header{Name: "B"}},
|
||||
Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}},
|
||||
RowEvents: render.RowEvents{
|
||||
render.RowEvent{
|
||||
Row: render.Row{
|
||||
|
|
@ -67,15 +67,18 @@ func TestMaxColumn(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
0,
|
||||
"A",
|
||||
MaxyPad{32, 6},
|
||||
},
|
||||
}
|
||||
|
||||
for _, u := range uu {
|
||||
pads := make(MaxyPad, len(u.t.Header))
|
||||
ComputeMaxColumns(pads, u.s, u.t.Header, u.t.RowEvents)
|
||||
assert.Equal(t, u.e, pads)
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
pads := make(MaxyPad, len(u.t.Header))
|
||||
ComputeMaxColumns(pads, u.s, u.t.Header, u.t.RowEvents)
|
||||
assert.Equal(t, u.e, pads)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -114,7 +117,7 @@ func TestPad(t *testing.T) {
|
|||
|
||||
func BenchmarkMaxColumn(b *testing.B) {
|
||||
table := render.TableData{
|
||||
Header: render.HeaderRow{render.Header{Name: "A"}, render.Header{Name: "B"}},
|
||||
Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}},
|
||||
RowEvents: render.RowEvents{
|
||||
render.RowEvent{
|
||||
Row: render.Row{
|
||||
|
|
@ -134,6 +137,6 @@ func BenchmarkMaxColumn(b *testing.B) {
|
|||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
ComputeMaxColumns(pads, 0, table.Header, table.RowEvents)
|
||||
ComputeMaxColumns(pads, "A", table.Header, table.RowEvents)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
|
|
@ -30,20 +31,22 @@ type (
|
|||
type Table struct {
|
||||
*SelectTable
|
||||
|
||||
actions KeyActions
|
||||
BaseTitle string
|
||||
Path string
|
||||
cmdBuff *CmdBuff
|
||||
styles *config.Styles
|
||||
sortCol SortColumn
|
||||
colorerFn render.ColorerFunc
|
||||
decorateFn DecorateFunc
|
||||
wide bool
|
||||
toast bool
|
||||
actions KeyActions
|
||||
gvr client.GVR
|
||||
Path string
|
||||
cmdBuff *CmdBuff
|
||||
styles *config.Styles
|
||||
viewSetting *config.ViewSetting
|
||||
sortCol SortColumn
|
||||
colorerFn render.ColorerFunc
|
||||
decorateFn DecorateFunc
|
||||
wide bool
|
||||
toast bool
|
||||
header render.Header
|
||||
}
|
||||
|
||||
// NewTable returns a new table view.
|
||||
func NewTable(gvr string) *Table {
|
||||
func NewTable(gvr client.GVR) *Table {
|
||||
return &Table{
|
||||
SelectTable: &SelectTable{
|
||||
Table: tview.NewTable(),
|
||||
|
|
@ -51,10 +54,10 @@ func NewTable(gvr string) *Table {
|
|||
selectedRow: 1,
|
||||
marks: make(map[string]struct{}),
|
||||
},
|
||||
actions: make(KeyActions),
|
||||
cmdBuff: NewCmdBuff('/', FilterBuff),
|
||||
BaseTitle: gvr,
|
||||
sortCol: SortColumn{index: -1, colCount: 0, asc: true},
|
||||
gvr: gvr,
|
||||
actions: make(KeyActions),
|
||||
cmdBuff: NewCmdBuff('/', FilterBuff),
|
||||
sortCol: SortColumn{asc: true},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,10 +72,22 @@ func (t *Table) Init(ctx context.Context) {
|
|||
t.SetInputCapture(t.keyboard)
|
||||
t.SetBackgroundColor(tcell.ColorDefault)
|
||||
|
||||
t.styles = mustExtractSyles(ctx)
|
||||
if cfg, ok := ctx.Value(internal.KeyViewConfig).(*config.CustomView); ok && cfg != nil {
|
||||
cfg.AddListener(t.GVR().String(), t)
|
||||
}
|
||||
t.styles = mustExtractStyles(ctx)
|
||||
t.StylesChanged(t.styles)
|
||||
}
|
||||
|
||||
// GVR returns a resource descriptor.
|
||||
func (t *Table) GVR() client.GVR { return t.gvr }
|
||||
|
||||
// ViewSettingsChanged notifies listener the view configuration changed.
|
||||
func (t *Table) ViewSettingsChanged(settings config.ViewSetting) {
|
||||
t.viewSetting = &settings
|
||||
t.Refresh()
|
||||
}
|
||||
|
||||
// StylesChanged notifies the skin changed.
|
||||
func (t *Table) StylesChanged(s *config.Styles) {
|
||||
t.SetBackgroundColor(s.Table().BgColor.Color())
|
||||
|
|
@ -178,12 +193,13 @@ func (t *Table) SetColorerFn(f render.ColorerFunc) {
|
|||
}
|
||||
|
||||
// SetSortCol sets in sort column index and order.
|
||||
func (t *Table) SetSortCol(index, count int, asc bool) {
|
||||
t.sortCol.index, t.sortCol.colCount, t.sortCol.asc = index, count, asc
|
||||
func (t *Table) SetSortCol(name string, asc bool) {
|
||||
t.sortCol.name, t.sortCol.asc = name, asc
|
||||
}
|
||||
|
||||
// Update table content.
|
||||
func (t *Table) Update(data render.TableData) {
|
||||
t.header = data.Header
|
||||
if t.decorateFn != nil {
|
||||
data = t.decorateFn(data)
|
||||
}
|
||||
|
|
@ -193,18 +209,36 @@ func (t *Table) Update(data render.TableData) {
|
|||
|
||||
func (t *Table) doUpdate(data render.TableData) {
|
||||
if client.IsAllNamespaces(data.Namespace) {
|
||||
t.actions[KeyShiftP] = NewKeyAction("Sort Namespace", t.SortColCmd(-2, true), false)
|
||||
t.actions[KeyShiftP] = NewKeyAction("Sort Namespace", t.SortColCmd("NAMESPACE", true), false)
|
||||
} else {
|
||||
t.actions.Delete(KeyShiftP)
|
||||
}
|
||||
|
||||
hasMX := t.model.HasMetrics()
|
||||
var cols []string
|
||||
if t.viewSetting != nil {
|
||||
cols = t.viewSetting.Columns
|
||||
}
|
||||
if len(cols) == 0 {
|
||||
cols = t.header.Columns(t.wide)
|
||||
}
|
||||
data = data.Customize(cols, t.wide)
|
||||
|
||||
if t.sortCol.name == "" || data.Header.IndexOf(t.sortCol.name, false) == -1 {
|
||||
t.sortCol.name = data.Header[0].Name
|
||||
}
|
||||
|
||||
t.Clear()
|
||||
t.adjustSorter(data)
|
||||
fg := t.styles.Table().Header.FgColor.Color()
|
||||
bg := t.styles.Table().Header.BgColor.Color()
|
||||
|
||||
var col int
|
||||
fmt.Printf("NS %q\n", t.GetModel().GetNamespace())
|
||||
for _, h := range data.Header {
|
||||
if h.Wide && !t.wide {
|
||||
if h.Name == "NAMESPACE" && !t.GetModel().ClusterWide() {
|
||||
continue
|
||||
}
|
||||
if h.MX && !hasMX {
|
||||
continue
|
||||
}
|
||||
t.AddHeaderCell(col, h)
|
||||
|
|
@ -213,35 +247,70 @@ func (t *Table) doUpdate(data render.TableData) {
|
|||
c.SetTextColor(fg)
|
||||
col++
|
||||
}
|
||||
data.RowEvents.Sort(data.Namespace, t.sortCol.index, t.sortCol.asc)
|
||||
data.RowEvents.Sort(data.Namespace, data.Header.IndexOf(t.sortCol.name, false), t.sortCol.name == "AGE", t.sortCol.asc)
|
||||
|
||||
pads := make(MaxyPad, len(data.Header))
|
||||
ComputeMaxColumns(pads, t.sortCol.index, data.Header, data.RowEvents)
|
||||
for i, r := range data.RowEvents {
|
||||
t.buildRow(data.Namespace, i+1, r, data.Header, pads)
|
||||
ComputeMaxColumns(pads, t.sortCol.name, data.Header, data.RowEvents)
|
||||
for row, re := range data.RowEvents {
|
||||
t.buildRow(row+1, re, data.Header, pads, hasMX)
|
||||
}
|
||||
t.updateSelection(true)
|
||||
}
|
||||
|
||||
// SortColCmd designates a sorted column.
|
||||
func (t *Table) SortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
var index int
|
||||
switch col {
|
||||
case -2:
|
||||
index = 0
|
||||
case -1:
|
||||
index = t.GetColumnCount() - 1
|
||||
case -3:
|
||||
index = t.GetColumnCount() - 2
|
||||
default:
|
||||
index = t.NameColIndex() + col
|
||||
func (t *Table) buildRow(r int, re render.RowEvent, h render.Header, pads MaxyPad, hasMX bool) {
|
||||
color := render.DefaultColorer
|
||||
if t.colorerFn != nil {
|
||||
color = t.colorerFn
|
||||
}
|
||||
|
||||
marked := t.IsMarked(re.Row.ID)
|
||||
var col int
|
||||
for c, field := range re.Row.Fields {
|
||||
if c >= len(h) {
|
||||
log.Error().Msgf("field/header overflow detected for %d::%d. Check your mappings!", c, len(h))
|
||||
continue
|
||||
}
|
||||
if h[c].Name == "NAMESPACE" && !t.GetModel().ClusterWide() {
|
||||
continue
|
||||
}
|
||||
if h[c].MX && !hasMX {
|
||||
continue
|
||||
}
|
||||
|
||||
if !re.Deltas.IsBlank() && !h.IsAgeCol(c) {
|
||||
field += Deltas(re.Deltas[c], field)
|
||||
}
|
||||
|
||||
if h[c].Decorator != nil {
|
||||
field = h[c].Decorator(field)
|
||||
}
|
||||
if h[c].Align == tview.AlignLeft {
|
||||
field = formatCell(field, pads[c])
|
||||
}
|
||||
|
||||
cell := tview.NewTableCell(field)
|
||||
cell.SetExpansion(1)
|
||||
cell.SetAlign(h[c].Align)
|
||||
cell.SetTextColor(color(t.GetModel().GetNamespace(), h, re))
|
||||
if marked {
|
||||
cell.SetTextColor(t.styles.Table().MarkColor.Color())
|
||||
}
|
||||
if col == 0 {
|
||||
cell.SetReference(re.Row.ID)
|
||||
}
|
||||
t.SetCell(r, col, cell)
|
||||
col++
|
||||
}
|
||||
}
|
||||
|
||||
// SortColCmd designates a sorted column.
|
||||
func (t *Table) SortColCmd(name string, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
t.sortCol.asc = !t.sortCol.asc
|
||||
if t.sortCol.index != index {
|
||||
if t.sortCol.name != name {
|
||||
t.sortCol.asc = asc
|
||||
}
|
||||
t.sortCol.index = index
|
||||
t.sortCol.name = name
|
||||
t.Refresh()
|
||||
return nil
|
||||
}
|
||||
|
|
@ -255,57 +324,6 @@ func (t *Table) SortInvertCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (t *Table) adjustSorter(data render.TableData) {
|
||||
// Going from namespace to non namespace or vice-versa?
|
||||
switch {
|
||||
case t.sortCol.colCount == 0:
|
||||
case len(data.Header) > t.sortCol.colCount:
|
||||
t.sortCol.index++
|
||||
case len(data.Header) < t.sortCol.colCount:
|
||||
t.sortCol.index--
|
||||
}
|
||||
t.sortCol.colCount = len(data.Header)
|
||||
if t.sortCol.index < 0 {
|
||||
t.sortCol.index = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) buildRow(ns string, r int, re render.RowEvent, header render.HeaderRow, pads MaxyPad) {
|
||||
color := render.DefaultColorer
|
||||
if t.colorerFn != nil {
|
||||
color = t.colorerFn
|
||||
}
|
||||
marked := t.IsMarked(re.Row.ID)
|
||||
var col int
|
||||
for c, field := range re.Row.Fields {
|
||||
if header[c].Wide && !t.wide {
|
||||
continue
|
||||
}
|
||||
if !re.Deltas.IsBlank() && !header.AgeCol(c) {
|
||||
field += Deltas(re.Deltas[c], field)
|
||||
}
|
||||
if header[c].Decorator != nil {
|
||||
field = header[c].Decorator(field)
|
||||
}
|
||||
if header[c].Align == tview.AlignLeft {
|
||||
field = formatCell(field, pads[c])
|
||||
}
|
||||
|
||||
cell := tview.NewTableCell(field)
|
||||
cell.SetExpansion(1)
|
||||
cell.SetAlign(header[c].Align)
|
||||
cell.SetTextColor(color(ns, re))
|
||||
if marked {
|
||||
cell.SetTextColor(t.styles.Table().MarkColor.Color())
|
||||
}
|
||||
if col == 0 {
|
||||
cell.SetReference(re.Row.ID)
|
||||
}
|
||||
t.SetCell(r, col, cell)
|
||||
col++
|
||||
}
|
||||
}
|
||||
|
||||
// ClearMarks clear out marked items.
|
||||
func (t *Table) ClearMarks() {
|
||||
t.SelectTable.ClearMarks()
|
||||
|
|
@ -314,13 +332,16 @@ func (t *Table) ClearMarks() {
|
|||
|
||||
// Refresh update the table data.
|
||||
func (t *Table) Refresh() {
|
||||
data := t.model.Peek()
|
||||
if len(data.Header) == 0 {
|
||||
return
|
||||
}
|
||||
// BOZO!! Really want to tell model reload now. Refactor!
|
||||
t.Update(t.model.Peek())
|
||||
t.Update(data)
|
||||
}
|
||||
|
||||
// GetSelectedRow returns the entire selected row.
|
||||
func (t *Table) GetSelectedRow() render.Row {
|
||||
log.Debug().Msgf("INDEX %d", t.GetSelectedRowIndex())
|
||||
return t.model.Peek().RowEvents[t.GetSelectedRowIndex()-1].Row
|
||||
}
|
||||
|
||||
|
|
@ -337,8 +358,9 @@ func (t *Table) NameColIndex() int {
|
|||
}
|
||||
|
||||
// AddHeaderCell configures a table cell header.
|
||||
func (t *Table) AddHeaderCell(col int, h render.Header) {
|
||||
c := tview.NewTableCell(sortIndicator(t.sortCol, t.styles.Table(), col, h.Name))
|
||||
func (t *Table) AddHeaderCell(col int, h render.HeaderColumn) {
|
||||
sortCol := h.Name == t.sortCol.name
|
||||
c := tview.NewTableCell(sortIndicator(sortCol, t.sortCol.asc, t.styles.Table(), h.Name))
|
||||
c.SetExpansion(1)
|
||||
c.SetAlign(h.Align)
|
||||
t.SetCell(0, col, c)
|
||||
|
|
@ -392,9 +414,9 @@ func (t *Table) styleTitle() string {
|
|||
rc--
|
||||
}
|
||||
|
||||
base := strings.Title(t.BaseTitle)
|
||||
base := strings.Title(t.gvr.R())
|
||||
ns := t.GetModel().GetNamespace()
|
||||
if client.IsAllNamespaces(ns) {
|
||||
if client.IsClusterWide(ns) || ns == client.NotNamespaced {
|
||||
ns = client.NamespaceAll
|
||||
}
|
||||
path := t.Path
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ var (
|
|||
fuzzyRx = regexp.MustCompile(`\A\-f`)
|
||||
)
|
||||
|
||||
func mustExtractSyles(ctx context.Context) *config.Styles {
|
||||
func mustExtractStyles(ctx context.Context) *config.Styles {
|
||||
styles, ok := ctx.Value(internal.KeyStyles).(*config.Styles)
|
||||
if !ok {
|
||||
log.Fatal().Msg("Expecting valid styles")
|
||||
|
|
@ -98,13 +98,13 @@ func SkinTitle(fmat string, style config.Frame) string {
|
|||
return fmat
|
||||
}
|
||||
|
||||
func sortIndicator(col SortColumn, style config.Table, index int, name string) string {
|
||||
if col.index != index {
|
||||
func sortIndicator(sort, asc bool, style config.Table, name string) string {
|
||||
if !sort {
|
||||
return name
|
||||
}
|
||||
|
||||
order := descIndicator
|
||||
if col.asc {
|
||||
if asc {
|
||||
order = ascIndicator
|
||||
}
|
||||
return fmt.Sprintf("%s[%s::b]%s[::]", name, style.Header.SorterColor, order)
|
||||
|
|
@ -119,7 +119,7 @@ func formatCell(field string, padding int) string {
|
|||
}
|
||||
|
||||
func filterToast(data render.TableData) render.TableData {
|
||||
validX := data.Header.IndexOf("VALID")
|
||||
validX := data.Header.IndexOf("VALID", true)
|
||||
if validX == -1 {
|
||||
return data
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
|
|
@ -15,28 +16,26 @@ import (
|
|||
)
|
||||
|
||||
func TestTableNew(t *testing.T) {
|
||||
v := ui.NewTable("fred")
|
||||
ctx := context.WithValue(context.Background(), internal.KeyStyles, config.NewStyles())
|
||||
v.Init(ctx)
|
||||
v := ui.NewTable(client.NewGVR("fred"))
|
||||
v.Init(makeContext())
|
||||
|
||||
assert.Equal(t, "fred", v.BaseTitle)
|
||||
assert.Equal(t, "fred", v.GVR().String())
|
||||
}
|
||||
|
||||
func TestTableUpdate(t *testing.T) {
|
||||
v := ui.NewTable("fred")
|
||||
ctx := context.WithValue(context.Background(), internal.KeyStyles, config.NewStyles())
|
||||
v.Init(ctx)
|
||||
v := ui.NewTable(client.NewGVR("fred"))
|
||||
v.Init(makeContext())
|
||||
|
||||
v.Update(makeTableData())
|
||||
data := makeTableData()
|
||||
v.Update(data)
|
||||
|
||||
assert.Equal(t, 3, v.GetRowCount())
|
||||
assert.Equal(t, 3, v.GetColumnCount())
|
||||
assert.Equal(t, len(data.RowEvents)+1, v.GetRowCount())
|
||||
assert.Equal(t, len(data.Header), v.GetColumnCount())
|
||||
}
|
||||
|
||||
func TestTableSelection(t *testing.T) {
|
||||
v := ui.NewTable("fred")
|
||||
ctx := context.WithValue(context.Background(), internal.KeyStyles, config.NewStyles())
|
||||
v.Init(ctx)
|
||||
v := ui.NewTable(client.NewGVR("fred"))
|
||||
v.Init(makeContext())
|
||||
m := &testModel{}
|
||||
v.SetModel(m)
|
||||
v.Update(m.Peek())
|
||||
|
|
@ -62,6 +61,7 @@ var _ ui.Tabular = &testModel{}
|
|||
|
||||
func (t *testModel) SetInstance(string) {}
|
||||
func (t *testModel) Empty() bool { return false }
|
||||
func (t *testModel) HasMetrics() bool { return true }
|
||||
func (t *testModel) Peek() render.TableData { return makeTableData() }
|
||||
func (t *testModel) ClusterWide() bool { return false }
|
||||
func (t *testModel) GetNamespace() string { return "blee" }
|
||||
|
|
@ -87,10 +87,10 @@ func (t *testModel) SetRefreshRate(time.Duration) {}
|
|||
func makeTableData() render.TableData {
|
||||
t := render.NewTableData()
|
||||
t.Namespace = ""
|
||||
t.Header = render.HeaderRow{
|
||||
render.Header{Name: "a"},
|
||||
render.Header{Name: "b"},
|
||||
render.Header{Name: "c"},
|
||||
t.Header = render.Header{
|
||||
render.HeaderColumn{Name: "A"},
|
||||
render.HeaderColumn{Name: "B"},
|
||||
render.HeaderColumn{Name: "C"},
|
||||
}
|
||||
t.RowEvents = render.RowEvents{
|
||||
render.RowEvent{
|
||||
|
|
@ -109,3 +109,10 @@ func makeTableData() render.TableData {
|
|||
|
||||
return *t
|
||||
}
|
||||
|
||||
func makeContext() context.Context {
|
||||
ctx := context.WithValue(context.Background(), internal.KeyStyles, config.NewStyles())
|
||||
ctx = context.WithValue(ctx, internal.KeyViewConfig, config.NewCustomView())
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,8 @@ type (
|
|||
|
||||
// SortColumn represents a sortable column.
|
||||
SortColumn struct {
|
||||
index int
|
||||
colCount int
|
||||
asc bool
|
||||
name string
|
||||
asc bool
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -61,6 +60,9 @@ type Tabular interface {
|
|||
// Peek returns current model data.
|
||||
Peek() render.TableData
|
||||
|
||||
// HasMetrics returns true if metrics are available on cluster.
|
||||
HasMetrics() bool
|
||||
|
||||
// Watch watches a given resource for changes.
|
||||
Watch(context.Context)
|
||||
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ func (a *Alias) bindKeys(aa ui.KeyActions) {
|
|||
aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)
|
||||
aa.Add(ui.KeyActions{
|
||||
tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, true),
|
||||
ui.KeyShiftR: ui.NewKeyAction("Sort Resource", a.GetTable().SortColCmd(0, true), false),
|
||||
ui.KeyShiftC: ui.NewKeyAction("Sort Command", a.GetTable().SortColCmd(1, true), false),
|
||||
ui.KeyShiftA: ui.NewKeyAction("Sort ApiGroup", a.GetTable().SortColCmd(2, true), false),
|
||||
ui.KeyShiftR: ui.NewKeyAction("Sort Resource", a.GetTable().SortColCmd("RESOURCE", true), false),
|
||||
ui.KeyShiftC: ui.NewKeyAction("Sort Command", a.GetTable().SortColCmd("COMMAND", true), false),
|
||||
ui.KeyShiftA: ui.NewKeyAction("Sort ApiGroup", a.GetTable().SortColCmd("APIGROUP", true), false),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ func TestAliasNew(t *testing.T) {
|
|||
|
||||
assert.Nil(t, v.Init(makeContext()))
|
||||
assert.Equal(t, "Aliases", v.Name())
|
||||
assert.Equal(t, 7, len(v.Hints()))
|
||||
assert.Equal(t, 6, len(v.Hints()))
|
||||
}
|
||||
|
||||
func TestAliasSearch(t *testing.T) {
|
||||
|
|
@ -101,6 +101,7 @@ var _ ui.Tabular = &testModel{}
|
|||
|
||||
func (t *testModel) SetInstance(string) {}
|
||||
func (t *testModel) Empty() bool { return false }
|
||||
func (t *testModel) HasMetrics() bool { return true }
|
||||
func (t *testModel) Peek() render.TableData { return makeTableData() }
|
||||
func (t *testModel) ClusterWide() bool { return false }
|
||||
func (t *testModel) GetNamespace() string { return "blee" }
|
||||
|
|
@ -127,10 +128,10 @@ func (t *testModel) SetRefreshRate(time.Duration) {}
|
|||
func makeTableData() render.TableData {
|
||||
return render.TableData{
|
||||
Namespace: client.ClusterScope,
|
||||
Header: render.HeaderRow{
|
||||
render.Header{Name: "RESOURCE"},
|
||||
render.Header{Name: "COMMAND"},
|
||||
render.Header{Name: "APIGROUP"},
|
||||
Header: render.Header{
|
||||
render.HeaderColumn{Name: "RESOURCE"},
|
||||
render.HeaderColumn{Name: "COMMAND"},
|
||||
render.HeaderColumn{Name: "APIGROUP"},
|
||||
},
|
||||
RowEvents: render.RowEvents{
|
||||
render.RowEvent{
|
||||
|
|
|
|||
|
|
@ -178,9 +178,15 @@ func (a *App) Halt() {
|
|||
func (a *App) Resume() {
|
||||
var ctx context.Context
|
||||
ctx, a.cancelFn = context.WithCancel(context.Background())
|
||||
|
||||
go a.clusterUpdater(ctx)
|
||||
if err := a.StylesUpdater(ctx, a); err != nil {
|
||||
log.Error().Err(err).Msgf("Styles update failed")
|
||||
|
||||
if err := a.StylesWatcher(ctx, a); err != nil {
|
||||
log.Error().Err(err).Msgf("Styles watcher failed")
|
||||
}
|
||||
|
||||
if err := a.CustomViewsWatcher(ctx, a); err != nil {
|
||||
log.Error().Err(err).Msgf("CustomView watcher failed")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -261,7 +267,6 @@ func (a *App) switchCtx(name string, loadPods bool) error {
|
|||
}
|
||||
a.initFactory(ns)
|
||||
|
||||
client.ResetMetrics()
|
||||
if err := a.command.Reset(true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ func NewBenchmark(gvr client.GVR) ResourceViewer {
|
|||
b.GetTable().SetBorderFocusColor(tcell.ColorSeaGreen)
|
||||
b.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorSeaGreen, tcell.AttrNone)
|
||||
b.GetTable().SetColorerFn(render.Benchmark{}.ColorerFunc())
|
||||
b.GetTable().SetSortCol(b.GetTable().NameColIndex()+7, 0, true)
|
||||
b.GetTable().SetSortCol(ageCol, true)
|
||||
b.SetContextFn(b.benchContext)
|
||||
b.GetTable().SetEnterFn(b.viewBench)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ type Browser struct {
|
|||
|
||||
// NewBrowser returns a new browser.
|
||||
func NewBrowser(gvr client.GVR) ResourceViewer {
|
||||
log.Debug().Msgf("BRO %q", gvr)
|
||||
return &Browser{
|
||||
Table: NewTable(gvr),
|
||||
}
|
||||
|
|
@ -40,18 +41,17 @@ func NewBrowser(gvr client.GVR) ResourceViewer {
|
|||
// Init watches all running pods in given namespace
|
||||
func (b *Browser) Init(ctx context.Context) error {
|
||||
var err error
|
||||
b.meta, err = dao.MetaAccess.MetaFor(b.gvr)
|
||||
b.meta, err = dao.MetaAccess.MetaFor(b.GVR())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.BaseTitle = b.meta.Kind
|
||||
|
||||
if err = b.Table.Init(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
ns := client.CleanseNamespace(b.app.Config.ActiveNamespace())
|
||||
if dao.IsK8sMeta(b.meta) && b.app.ConOK() {
|
||||
if _, e := b.app.factory.CanForResource(ns, b.GVR(), client.MonitorAccess); e != nil {
|
||||
if _, e := b.app.factory.CanForResource(ns, b.GVR().String(), client.MonitorAccess); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ func (b *Browser) Init(ctx context.Context) error {
|
|||
if b.bindKeysFn != nil {
|
||||
b.bindKeysFn(b.Actions())
|
||||
}
|
||||
b.accessor, err = dao.AccessorFor(b.app.factory, b.gvr)
|
||||
b.accessor, err = dao.AccessorFor(b.app.factory, b.GVR())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -173,7 +173,7 @@ func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
ctx := b.defaultContext()
|
||||
raw, err := b.GetModel().ToYAML(ctx, path)
|
||||
if err != nil {
|
||||
b.App().Flash().Errf("unable to get resource %q -- %s", b.gvr, err)
|
||||
b.App().Flash().Errf("unable to get resource %q -- %s", b.GVR(), err)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -231,7 +231,7 @@ func (b *Browser) enterCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
if b.enterFn != nil {
|
||||
f = b.enterFn
|
||||
}
|
||||
f(b.app, b.GetModel(), b.gvr.String(), path)
|
||||
f(b.app, b.GetModel(), b.GVR().String(), path)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -252,9 +252,9 @@ func (b *Browser) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
b.Stop()
|
||||
defer b.Start()
|
||||
{
|
||||
msg := fmt.Sprintf("Delete %s %s?", b.gvr.R(), selections[0])
|
||||
msg := fmt.Sprintf("Delete %s %s?", b.GVR().R(), selections[0])
|
||||
if len(selections) > 1 {
|
||||
msg = fmt.Sprintf("Delete %d marked %s?", len(selections), b.gvr)
|
||||
msg = fmt.Sprintf("Delete %d marked %s?", len(selections), b.GVR())
|
||||
}
|
||||
if !dao.IsK8sMeta(b.meta) {
|
||||
b.simpleDelete(selections, msg)
|
||||
|
|
@ -271,7 +271,7 @@ func (b *Browser) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
if path == "" {
|
||||
return evt
|
||||
}
|
||||
describeResource(b.app, b.GetModel(), b.gvr.String(), path)
|
||||
describeResource(b.app, b.GetModel(), b.GVR().String(), path)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -284,7 +284,7 @@ func (b *Browser) editCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
ns, n := client.Namespaced(path)
|
||||
|
||||
if ok, err := b.app.Conn().CanI(ns, b.GVR(), []string{"edit"}); !ok || err != nil {
|
||||
if ok, err := b.app.Conn().CanI(ns, b.GVR().String(), []string{"edit"}); !ok || err != nil {
|
||||
b.App().Flash().Err(fmt.Errorf("Current user can't edit resource %s", b.GVR()))
|
||||
return nil
|
||||
}
|
||||
|
|
@ -312,7 +312,7 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
ns := b.namespaces[i]
|
||||
|
||||
auth, err := b.App().factory.Client().CanI(ns, b.GVR(), client.MonitorAccess)
|
||||
auth, err := b.App().factory.Client().CanI(ns, b.GVR().String(), client.MonitorAccess)
|
||||
if !auth {
|
||||
if err == nil {
|
||||
err = fmt.Errorf("current user can't access namespace %s", ns)
|
||||
|
|
@ -355,7 +355,7 @@ func (b *Browser) setNamespace(ns string) {
|
|||
func (b *Browser) defaultContext() context.Context {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, internal.KeyFactory, b.app.factory)
|
||||
ctx = context.WithValue(ctx, internal.KeyGVR, b.gvr.String())
|
||||
ctx = context.WithValue(ctx, internal.KeyGVR, b.GVR().String())
|
||||
ctx = context.WithValue(ctx, internal.KeyPath, b.Path)
|
||||
|
||||
ctx = context.WithValue(ctx, internal.KeyLabels, "")
|
||||
|
|
@ -424,9 +424,9 @@ func (b *Browser) simpleDelete(selections []string, msg string) {
|
|||
dialog.ShowConfirm(b.app.Content.Pages, "Confirm Delete", msg, func() {
|
||||
b.ShowDeleted()
|
||||
if len(selections) > 1 {
|
||||
b.app.Flash().Infof("Delete %d marked %s", len(selections), b.gvr)
|
||||
b.app.Flash().Infof("Delete %d marked %s", len(selections), b.GVR())
|
||||
} else {
|
||||
b.app.Flash().Infof("Delete resource %s %s", b.gvr, selections[0])
|
||||
b.app.Flash().Infof("Delete resource %s %s", b.GVR(), selections[0])
|
||||
}
|
||||
for _, sel := range selections {
|
||||
nuker, ok := b.accessor.(dao.Nuker)
|
||||
|
|
@ -448,9 +448,9 @@ func (b *Browser) resourceDelete(selections []string, msg string) {
|
|||
dialog.ShowDelete(b.app.Content.Pages, msg, func(cascade, force bool) {
|
||||
b.ShowDeleted()
|
||||
if len(selections) > 1 {
|
||||
b.app.Flash().Infof("Delete %d marked %s", len(selections), b.gvr)
|
||||
b.app.Flash().Infof("Delete %d marked %s", len(selections), b.GVR())
|
||||
} else {
|
||||
b.app.Flash().Infof("Delete resource %s %s", b.gvr, selections[0])
|
||||
b.app.Flash().Infof("Delete resource %s %s", b.GVR(), selections[0])
|
||||
}
|
||||
for _, sel := range selections {
|
||||
if err := b.GetModel().Delete(b.defaultContext(), sel, cascade, force); err != nil {
|
||||
|
|
|
|||
|
|
@ -37,9 +37,9 @@ func (c *Chart) bindKeys(aa ui.KeyActions) {
|
|||
aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)
|
||||
aa.Add(ui.KeyActions{
|
||||
ui.KeyB: ui.NewKeyAction("Blee", c.bleeCmd, true),
|
||||
ui.KeyShiftN: ui.NewKeyAction("Sort Name", c.GetTable().SortColCmd(0, true), false),
|
||||
ui.KeyShiftS: ui.NewKeyAction("Sort Status", c.GetTable().SortColCmd(2, true), false),
|
||||
ui.KeyShiftA: ui.NewKeyAction("Sort Age", c.GetTable().SortColCmd(-1, true), false),
|
||||
ui.KeyShiftN: ui.NewKeyAction("Sort Name", c.GetTable().SortColCmd(nameCol, true), false),
|
||||
ui.KeyShiftS: ui.NewKeyAction("Sort Status", c.GetTable().SortColCmd(statusCol, true), false),
|
||||
ui.KeyShiftA: ui.NewKeyAction("Sort Age", c.GetTable().SortColCmd(ageCol, true), false),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,8 +44,11 @@ func (c *ClusterInfo) StylesChanged(s *config.Styles) {
|
|||
}
|
||||
|
||||
func (c *ClusterInfo) layout() {
|
||||
for row, v := range []string{"Context", "Cluster", "User", "K9s Rev", "K8s Rev", "CPU", "MEM"} {
|
||||
c.SetCell(row, 0, c.sectionCell(v))
|
||||
for row, section := range []string{"Context", "Cluster", "User", "K9s Rev", "K8s Rev", "CPU", "MEM"} {
|
||||
if (section == "CPU" || section == "MEM") && !c.app.Conn().HasMetrics() {
|
||||
continue
|
||||
}
|
||||
c.SetCell(row, 0, c.sectionCell(section))
|
||||
c.SetCell(row, 1, c.infoCell(render.NAValue))
|
||||
}
|
||||
}
|
||||
|
|
@ -53,13 +56,6 @@ func (c *ClusterInfo) layout() {
|
|||
func (c *ClusterInfo) sectionCell(t string) *tview.TableCell {
|
||||
cell := tview.NewTableCell(t + ":")
|
||||
cell.SetAlign(tview.AlignLeft)
|
||||
// var style tcell.Style
|
||||
// style.Bold(true).
|
||||
// Background(tcell.ColorGreen).
|
||||
// Foreground(config.AsColor(c.styles.K9s.Info.SectionColor))
|
||||
// cell.SetStyle(style)
|
||||
// cell.SetBackgroundColor(c.app.Styles.BgColor())
|
||||
// cell.SetBackgroundColor(tcell.ColorDefault)
|
||||
cell.SetBackgroundColor(tcell.ColorGreen)
|
||||
|
||||
return cell
|
||||
|
|
@ -76,44 +72,28 @@ func (c *ClusterInfo) infoCell(t string) *tview.TableCell {
|
|||
|
||||
// ClusterInfoUpdated notifies the cluster meta was updated.
|
||||
func (c *ClusterInfo) ClusterInfoUpdated(data model.ClusterMeta) {
|
||||
c.app.QueueUpdateDraw(func() {
|
||||
var row int
|
||||
c.GetCell(row, 1).SetText(data.Context)
|
||||
row++
|
||||
c.GetCell(row, 1).SetText(data.Cluster)
|
||||
row++
|
||||
c.GetCell(row, 1).SetText(data.User)
|
||||
row++
|
||||
c.GetCell(row, 1).SetText(data.K9sVer)
|
||||
row++
|
||||
c.GetCell(row, 1).SetText(data.K8sVer)
|
||||
row++
|
||||
c.GetCell(row, 1).SetText(render.AsPerc(data.Cpu) + "%")
|
||||
row++
|
||||
c.GetCell(row, 1).SetText(render.AsPerc(data.Mem) + "%")
|
||||
c.ClusterInfoChanged(data, data)
|
||||
}
|
||||
|
||||
c.updateStyle()
|
||||
})
|
||||
func (c *ClusterInfo) setCell(row int, s string) int {
|
||||
c.GetCell(row, 1).SetText(s)
|
||||
return row + 1
|
||||
}
|
||||
|
||||
// ClusterInfoChanged notifies the cluster meta was changed.
|
||||
func (c *ClusterInfo) ClusterInfoChanged(prev, curr model.ClusterMeta) {
|
||||
c.app.QueueUpdateDraw(func() {
|
||||
var row int
|
||||
c.GetCell(row, 1).SetText(curr.Context)
|
||||
row++
|
||||
c.GetCell(row, 1).SetText(curr.Cluster)
|
||||
row++
|
||||
c.GetCell(row, 1).SetText(curr.User)
|
||||
row++
|
||||
c.GetCell(row, 1).SetText(curr.K9sVer)
|
||||
row++
|
||||
c.GetCell(row, 1).SetText(curr.K8sVer)
|
||||
row++
|
||||
c.GetCell(row, 1).SetText(ui.AsPercDelta(prev.Cpu, curr.Cpu))
|
||||
row++
|
||||
c.GetCell(row, 1).SetText(ui.AsPercDelta(prev.Mem, curr.Mem))
|
||||
|
||||
c.Clear()
|
||||
c.layout()
|
||||
row := c.setCell(0, curr.Context)
|
||||
row = c.setCell(row, curr.Cluster)
|
||||
row = c.setCell(row, curr.User)
|
||||
row = c.setCell(row, curr.K9sVer)
|
||||
row = c.setCell(row, curr.K8sVer)
|
||||
if c.app.Conn().HasMetrics() {
|
||||
row = c.setCell(row, ui.AsPercDelta(prev.Cpu, curr.Cpu))
|
||||
_ = c.setCell(row, ui.AsPercDelta(prev.Mem, curr.Mem))
|
||||
}
|
||||
c.updateStyle()
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,10 @@ import (
|
|||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
containerTitle = "Containers"
|
||||
portsCol = 13
|
||||
)
|
||||
const containerTitle = "Containers"
|
||||
|
||||
// Container represents a container view.
|
||||
type Container struct {
|
||||
|
|
@ -53,12 +51,12 @@ func (c *Container) bindKeys(aa ui.KeyActions) {
|
|||
|
||||
aa.Add(ui.KeyActions{
|
||||
ui.KeyShiftF: ui.NewKeyAction("PortForward", c.portFwdCmd, true),
|
||||
ui.KeyShiftC: ui.NewKeyAction("Sort CPU", c.GetTable().SortColCmd(6, false), false),
|
||||
ui.KeyShiftM: ui.NewKeyAction("Sort MEM", c.GetTable().SortColCmd(7, false), false),
|
||||
ui.KeyShiftX: ui.NewKeyAction("Sort %CPU (REQ)", c.GetTable().SortColCmd(8, false), false),
|
||||
ui.KeyShiftZ: ui.NewKeyAction("Sort %MEM (REQ)", c.GetTable().SortColCmd(9, false), false),
|
||||
tcell.KeyCtrlX: ui.NewKeyAction("Sort %CPU (LIM)", c.GetTable().SortColCmd(8, false), false),
|
||||
tcell.KeyCtrlQ: ui.NewKeyAction("Sort %MEM (LIM)", c.GetTable().SortColCmd(9, false), false),
|
||||
ui.KeyShiftC: ui.NewKeyAction("Sort CPU", c.GetTable().SortColCmd(cpuCol, false), false),
|
||||
ui.KeyShiftM: ui.NewKeyAction("Sort MEM", c.GetTable().SortColCmd(memCol, false), false),
|
||||
ui.KeyShiftX: ui.NewKeyAction("Sort %CPU (REQ)", c.GetTable().SortColCmd("%CPU/R", false), false),
|
||||
ui.KeyShiftZ: ui.NewKeyAction("Sort %MEM (REQ)", c.GetTable().SortColCmd("%MEM/R", false), false),
|
||||
tcell.KeyCtrlX: ui.NewKeyAction("Sort %CPU (LIM)", c.GetTable().SortColCmd("%CPU/L", false), false),
|
||||
tcell.KeyCtrlQ: ui.NewKeyAction("Sort %MEM (LIM)", c.GetTable().SortColCmd("%MEM/L", false), false),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -131,13 +129,41 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
|
||||
func (c *Container) isForwardable(path string) ([]string, bool) {
|
||||
state := c.GetTable().GetSelectedCell(3)
|
||||
if state != "Running" {
|
||||
po, err := fetchPod(c.App().factory, c.GetTable().Path)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
cc := po.Spec.Containers
|
||||
var co *v1.Container
|
||||
for i := range cc {
|
||||
if cc[i].Name == path {
|
||||
co = &cc[i]
|
||||
}
|
||||
}
|
||||
if co == nil {
|
||||
log.Error().Err(fmt.Errorf("unable to locate container named %q", path))
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var cs *v1.ContainerStatus
|
||||
ss := po.Status.ContainerStatuses
|
||||
for i := range ss {
|
||||
if ss[i].Name == path {
|
||||
cs = &ss[i]
|
||||
}
|
||||
}
|
||||
if cs == nil {
|
||||
log.Error().Err(fmt.Errorf("unable to locate container status named %q", path))
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if render.ToContainerState(cs.State) != "Running" {
|
||||
c.App().Flash().Err(fmt.Errorf("Container %s is not running?", path))
|
||||
return nil, false
|
||||
}
|
||||
|
||||
portC := c.GetTable().GetSelectedCell(portsCol)
|
||||
portC := render.ToContainerPorts(co.Ports)
|
||||
ports := strings.Split(portC, ",")
|
||||
if len(ports) == 0 {
|
||||
c.App().Flash().Err(errors.New("Container exposes no ports"))
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@ func TestContainerNew(t *testing.T) {
|
|||
|
||||
assert.Nil(t, c.Init(makeCtx()))
|
||||
assert.Equal(t, "Containers", c.Name())
|
||||
assert.Equal(t, 16, len(c.Hints()))
|
||||
assert.Equal(t, 15, len(c.Hints()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@ func TestContext(t *testing.T) {
|
|||
|
||||
assert.Nil(t, ctx.Init(makeCtx()))
|
||||
assert.Equal(t, "Contexts", ctx.Name())
|
||||
assert.Equal(t, 4, len(ctx.Hints()))
|
||||
assert.Equal(t, 3, len(ctx.Hints()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ func (c *CronJob) trigger(evt *tcell.EventKey) *tcell.EventKey {
|
|||
return evt
|
||||
}
|
||||
|
||||
res, err := dao.AccessorFor(c.App().factory, client.NewGVR(c.GVR()))
|
||||
res, err := dao.AccessorFor(c.App().factory, c.GVR())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,15 +38,15 @@ func NewDeploy(gvr client.GVR) ResourceViewer {
|
|||
|
||||
func (d *Deploy) bindKeys(aa ui.KeyActions) {
|
||||
aa.Add(ui.KeyActions{
|
||||
ui.KeyShiftR: ui.NewKeyAction("Sort Ready", d.GetTable().SortColCmd(1, true), false),
|
||||
ui.KeyShiftU: ui.NewKeyAction("Sort UpToDate", d.GetTable().SortColCmd(2, true), false),
|
||||
ui.KeyShiftL: ui.NewKeyAction("Sort Available", d.GetTable().SortColCmd(3, true), false),
|
||||
ui.KeyShiftR: ui.NewKeyAction("Sort Ready", d.GetTable().SortColCmd(readyCol, true), false),
|
||||
ui.KeyShiftU: ui.NewKeyAction("Sort UpToDate", d.GetTable().SortColCmd(uptodateCol, true), false),
|
||||
ui.KeyShiftL: ui.NewKeyAction("Sort Available", d.GetTable().SortColCmd(availCol, true), false),
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Deploy) showPods(app *App, model ui.Tabular, gvr, path string) {
|
||||
var res dao.Deployment
|
||||
res.Init(app.factory, client.NewGVR(d.GVR()))
|
||||
res.Init(app.factory, d.GVR())
|
||||
|
||||
dp, err := res.GetInstance(path)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@ func TestDeploy(t *testing.T) {
|
|||
|
||||
assert.Nil(t, v.Init(makeCtx()))
|
||||
assert.Equal(t, "Deployments", v.Name())
|
||||
assert.Equal(t, 12, len(v.Hints()))
|
||||
assert.Equal(t, 11, len(v.Hints()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,17 +30,17 @@ func NewDaemonSet(gvr client.GVR) ResourceViewer {
|
|||
|
||||
func (d *DaemonSet) bindKeys(aa ui.KeyActions) {
|
||||
aa.Add(ui.KeyActions{
|
||||
ui.KeyShiftD: ui.NewKeyAction("Sort Desired", d.GetTable().SortColCmd(1, true), false),
|
||||
ui.KeyShiftC: ui.NewKeyAction("Sort Current", d.GetTable().SortColCmd(2, true), false),
|
||||
ui.KeyShiftR: ui.NewKeyAction("Sort Ready", d.GetTable().SortColCmd(3, true), false),
|
||||
ui.KeyShiftU: ui.NewKeyAction("Sort UpToDate", d.GetTable().SortColCmd(4, true), false),
|
||||
ui.KeyShiftL: ui.NewKeyAction("Sort Available", d.GetTable().SortColCmd(5, true), false),
|
||||
ui.KeyShiftD: ui.NewKeyAction("Sort Desired", d.GetTable().SortColCmd("DESIRED", true), false),
|
||||
ui.KeyShiftC: ui.NewKeyAction("Sort Current", d.GetTable().SortColCmd("CURRENT", true), false),
|
||||
ui.KeyShiftR: ui.NewKeyAction("Sort Ready", d.GetTable().SortColCmd(readyCol, true), false),
|
||||
ui.KeyShiftU: ui.NewKeyAction("Sort UpToDate", d.GetTable().SortColCmd(uptodateCol, true), false),
|
||||
ui.KeyShiftL: ui.NewKeyAction("Sort Available", d.GetTable().SortColCmd(availCol, true), false),
|
||||
})
|
||||
}
|
||||
|
||||
func (d *DaemonSet) showPods(app *App, model ui.Tabular, _, path string) {
|
||||
var res dao.DaemonSet
|
||||
res.Init(app.factory, client.NewGVR(d.GVR()))
|
||||
res.Init(app.factory, d.GVR())
|
||||
|
||||
ds, err := res.GetInstance(path)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@ func TestDaemonSet(t *testing.T) {
|
|||
|
||||
assert.Nil(t, v.Init(makeCtx()))
|
||||
assert.Equal(t, "DaemonSets", v.Name())
|
||||
assert.Equal(t, 13, len(v.Hints()))
|
||||
assert.Equal(t, 12, len(v.Hints()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ func NewEvent(gvr client.GVR) ResourceViewer {
|
|||
}
|
||||
e.GetTable().SetColorerFn(render.Event{}.ColorerFunc())
|
||||
e.SetBindKeysFn(e.bindKeys)
|
||||
e.GetTable().SetSortCol(7, 0, true)
|
||||
e.GetTable().SetSortCol(ageCol, true)
|
||||
|
||||
return &e
|
||||
}
|
||||
|
|
@ -27,9 +27,9 @@ func NewEvent(gvr client.GVR) ResourceViewer {
|
|||
func (e *Event) bindKeys(aa ui.KeyActions) {
|
||||
aa.Delete(tcell.KeyCtrlD, ui.KeyE)
|
||||
aa.Add(ui.KeyActions{
|
||||
ui.KeyShiftY: ui.NewKeyAction("Sort Type", e.GetTable().SortColCmd(1, true), false),
|
||||
ui.KeyShiftR: ui.NewKeyAction("Sort Reason", e.GetTable().SortColCmd(2, true), false),
|
||||
ui.KeyShiftE: ui.NewKeyAction("Sort Source", e.GetTable().SortColCmd(3, true), false),
|
||||
ui.KeyShiftC: ui.NewKeyAction("Sort Count", e.GetTable().SortColCmd(4, true), false),
|
||||
ui.KeyShiftY: ui.NewKeyAction("Sort Type", e.GetTable().SortColCmd("TYPE", true), false),
|
||||
ui.KeyShiftR: ui.NewKeyAction("Sort Reason", e.GetTable().SortColCmd("REASON", true), false),
|
||||
ui.KeyShiftE: ui.NewKeyAction("Sort Source", e.GetTable().SortColCmd("SOURCE", true), false),
|
||||
ui.KeyShiftC: ui.NewKeyAction("Sort Count", e.GetTable().SortColCmd("COUNT", true), false),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ func (g *Group) bindKeys(aa ui.KeyActions) {
|
|||
aa.Delete(ui.KeyShiftA, ui.KeyShiftP, tcell.KeyCtrlSpace, ui.KeySpace)
|
||||
aa.Add(ui.KeyActions{
|
||||
tcell.KeyEnter: ui.NewKeyAction("Rules", g.policyCmd, true),
|
||||
ui.KeyShiftK: ui.NewKeyAction("Sort Kind", g.GetTable().SortColCmd(1, true), false),
|
||||
ui.KeyShiftK: ui.NewKeyAction("Sort Kind", g.GetTable().SortColCmd("KIND", true), false),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ func TestHelp(t *testing.T) {
|
|||
v := view.NewHelp()
|
||||
|
||||
assert.Nil(t, v.Init(ctx))
|
||||
assert.Equal(t, 23, v.GetRowCount())
|
||||
assert.Equal(t, 22, v.GetRowCount())
|
||||
assert.Equal(t, 8, v.GetColumnCount())
|
||||
assert.Equal(t, "<a>", strings.TrimSpace(v.GetCell(1, 0).Text))
|
||||
assert.Equal(t, "Attach", strings.TrimSpace(v.GetCell(1, 1).Text))
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ func podCtx(app *App, path, labelSel, fieldSel string) ContextFunc {
|
|||
mx := client.NewMetricsServer(app.factory.Client())
|
||||
nmx, err := mx.FetchPodsMetrics(ns)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("No pods metrics")
|
||||
log.Debug().Err(err).Msgf("No pods metrics")
|
||||
}
|
||||
ctx = context.WithValue(ctx, internal.KeyMetrics, nmx)
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ func (l *LogsExtender) showLogs(path string, prev bool) {
|
|||
if l.containerFn != nil {
|
||||
co = l.containerFn()
|
||||
}
|
||||
if err := l.App().inject(NewLog(client.NewGVR(l.GVR()), path, co, prev)); err != nil {
|
||||
if err := l.App().inject(NewLog(l.GVR(), path, co, prev)); err != nil {
|
||||
l.App().Flash().Err(err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@ func (n *Node) bindKeys(aa ui.KeyActions) {
|
|||
aa.Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlD)
|
||||
aa.Add(ui.KeyActions{
|
||||
ui.KeyY: ui.NewKeyAction("YAML", n.viewCmd, true),
|
||||
ui.KeyShiftC: ui.NewKeyAction("Sort CPU", n.GetTable().SortColCmd(7, false), false),
|
||||
ui.KeyShiftM: ui.NewKeyAction("Sort MEM", n.GetTable().SortColCmd(8, false), false),
|
||||
ui.KeyShiftX: ui.NewKeyAction("Sort CPU%", n.GetTable().SortColCmd(9, false), false),
|
||||
ui.KeyShiftZ: ui.NewKeyAction("Sort MEM%", n.GetTable().SortColCmd(10, false), false),
|
||||
ui.KeyShiftC: ui.NewKeyAction("Sort CPU", n.GetTable().SortColCmd(cpuCol, false), false),
|
||||
ui.KeyShiftM: ui.NewKeyAction("Sort MEM", n.GetTable().SortColCmd(memCol, false), false),
|
||||
ui.KeyShiftX: ui.NewKeyAction("Sort CPU%", n.GetTable().SortColCmd("%CPU", false), false),
|
||||
ui.KeyShiftZ: ui.NewKeyAction("Sort MEM%", n.GetTable().SortColCmd("%MEM", false), false),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ func (n *Node) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
|
||||
sel := n.GetTable().GetSelectedItem()
|
||||
gvr := client.NewGVR(n.GVR()).GVR()
|
||||
gvr := n.GVR().GVR()
|
||||
o, err := n.App().factory.Client().DynDialOrDie().Resource(gvr).Get(sel, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
n.App().Flash().Errf("Unable to get resource %q -- %s", n.GVR(), err)
|
||||
|
|
|
|||
|
|
@ -78,7 +78,6 @@ func (n *Namespace) decorate(data render.TableData) render.TableData {
|
|||
|
||||
// checks if all ns is in the list if not add it.
|
||||
if _, ok := data.RowEvents.FindIndex(client.NamespaceAll); !ok {
|
||||
log.Debug().Msg("YO!!")
|
||||
data.RowEvents = append(data.RowEvents,
|
||||
render.RowEvent{
|
||||
Kind: render.EventUnchanged,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue