added toggle header + alias command fixes

mine
derailed 2019-10-01 15:31:17 -06:00
parent 7d05e4401e
commit 5baa447e46
62 changed files with 841 additions and 698 deletions

1
go.sum
View File

@ -386,6 +386,7 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM
github.com/storageos/go-api v0.0.0-20180912212459-343b3eff91fc/go.mod h1:ZrLn+e0ZuF3Y65PNF6dIwbJPZqfmtCXxFm9ckv0agOY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=

122
internal/config/alias.go Normal file
View File

@ -0,0 +1,122 @@
package config
import (
"io/ioutil"
"path/filepath"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v2"
)
// K9sAlias stores K9s command aliases.
var K9sAlias = filepath.Join(K9sHome, "alias.yml")
type Alias map[string]string
// Aliases represents a collection of aliases.
type Aliases struct {
Alias Alias `yaml:"alias"`
}
// NewAliases return a new alias.
func NewAliases() Aliases {
aa := Aliases{Alias: make(Alias, 50)}
aa.loadDefaults()
return aa
}
func (a Aliases) loadDefaults() {
a.Alias["dp"] = "apps/v1/deployments"
a.Alias["sec"] = "v1/secrets"
a.Alias["jo"] = "batch/v1/jobs"
a.Alias["cr"] = "rbac.authorization.k8s.io/v1/clusterroles"
a.Alias["crb"] = "rbac.authorization.k8s.io/v1/clusterrolebindings"
a.Alias["ro"] = "rbac.authorization.k8s.io/v1/roles"
a.Alias["rob"] = "rbac.authorization.k8s.io/v1/rolebindings"
a.Alias["np"] = "networking.k8s.io/v1beta1/rolebindings"
{
a.Alias["ctx"] = "contexts"
a.Alias["contexts"] = "contexts"
a.Alias["context"] = "contexts"
}
{
a.Alias["usr"] = "users"
a.Alias["users"] = "users"
a.Alias["user"] = "user"
}
{
a.Alias["grp"] = "groups"
a.Alias["group"] = "groups"
a.Alias["groups"] = "groups"
}
{
a.Alias["pf"] = "portforwards"
a.Alias["portforwards"] = "portforwards"
a.Alias["portforward"] = "portforwards"
}
{
a.Alias["be"] = "benchmarks"
a.Alias["benchmark"] = "benchmarks"
a.Alias["benchmarks"] = "benchmarks"
}
{
a.Alias["sd"] = "screendumps"
a.Alias["screendump"] = "screendumps"
a.Alias["screendumps"] = "screendumps"
}
}
// Load K9s aliases.
func (a Aliases) Load() error {
return a.LoadAliases(K9sAlias)
}
// Get retrieves an alias.
func (a Aliases) Get(k string) (string, bool) {
v, ok := a.Alias[k]
return v, ok
}
// Define declares a new alias.
func (a Aliases) Define(args ...string) {
if len(args)%2 != 0 {
panic("Invalid alias definition. You must specify pairs")
}
for i := 0; i < len(args); i += 2 {
a.Alias[args[i]] = args[i+1]
}
}
// LoadAliases K9s alias from a given file.
func (a Aliases) LoadAliases(path string) error {
f, err := ioutil.ReadFile(path)
if err != nil {
return err
}
var aa Aliases
if err := yaml.Unmarshal(f, &aa); err != nil {
return err
}
for k, v := range aa.Alias {
a.Alias[k] = v
}
return nil
}
// Save alias to disk.
func (a Aliases) Save() error {
log.Debug().Msg("[Config] Saving Aliases...")
return a.SaveAliases(K9sAlias)
}
// SaveAliases saves aliases to a given file.
func (a Aliases) SaveAliases(path string) error {
EnsurePath(path, DefaultDirMod)
cfg, err := yaml.Marshal(a)
if err != nil {
return err
}
return ioutil.WriteFile(path, cfg, 0644)
}

View File

@ -0,0 +1,26 @@
package config_test
import (
"testing"
"github.com/derailed/k9s/internal/config"
"github.com/stretchr/testify/assert"
)
func TestAliasesLoad(t *testing.T) {
aa := config.NewAliases()
assert.Nil(t, aa.LoadAliases("test_assets/alias.yml"))
assert.Equal(t, 27, len(aa.Alias))
}
func TestAliasesSave(t *testing.T) {
aa := config.NewAliases()
aa.Alias["test"] = "fred"
aa.Alias["blee"] = "duh"
aa.SaveAliases("/tmp/a.yml")
assert.Nil(t, aa.LoadAliases("/tmp/a.yml"))
assert.Equal(t, 28, len(aa.Alias))
}

View File

@ -30,15 +30,24 @@ var (
)
type (
// Connection present a kubernetes api server connection.
// Connection represents a kubernetes api server connection.
Connection k8s.Connection
// KubeSettings exposes kubeconfig context informations.
// KubeSettings exposes kubeconfig context information.
KubeSettings interface {
// CurrentContextName returns the name of the current context.
CurrentContextName() (string, error)
// CurrentClusterName returns the name of the current cluster.
CurrentClusterName() (string, error)
// CurrentNamespace returns the name of the current namespace.
CurrentNamespaceName() (string, error)
// ClusterNames() returns all available cluster names.
ClusterNames() ([]string, error)
// NamespaceNames returns all available namespace names.
NamespaceNames(nn []v1.Namespace) []string
}
@ -180,7 +189,6 @@ func (c *Config) Load(path string) error {
if cfg.K9s != nil {
c.K9s = cfg.K9s
}
log.Debug().Msgf("Headless ? %t", c.K9s.Headless)
return nil
}

View File

@ -8,7 +8,7 @@ const (
DefaultLogLevel = "info"
// DefaultCommand represents the default command to run.
DefaultCommand = "po"
DefaultCommand = ""
)
// Flags represents K9s configuration flags.

View File

@ -80,7 +80,6 @@ func (k *K9s) ActiveCluster() *Cluster {
if c, ok := k.Clusters[k.CurrentCluster]; ok {
return c
}
k.Clusters[k.CurrentCluster] = NewCluster()
return k.Clusters[k.CurrentCluster]

View File

@ -0,0 +1,3 @@
alias:
dp: "apps.v1.deployments"
pe: ".v1.pods"

62
internal/k8s/gvr.go Normal file
View File

@ -0,0 +1,62 @@
package k8s
import (
"path"
"strings"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// GVR represents a fully qualified kubernetes resource.
type GVR string
// NewGVR returns a new gvr.
func NewGVR(g, v, r string) GVR {
return GVR(path.Join(g, v, r))
}
// ToGVR returns a new gvr from a string.
func ToGVR(gv, r string) GVR {
return GVR(path.Join(gv, r))
}
// ResName returns a full res name ie dp.v1.apps.
func (g GVR) ResName() string {
return g.ToR() + "." + g.ToV() + "." + g.ToG()
}
// AsGR returns the group version.
func (g GVR) AsGR() schema.GroupVersion {
return schema.GroupVersion{
Group: g.ToG(),
Version: g.ToV(),
}
}
// String returns a GVR as a string.
func (g GVR) String() string {
return string(g)
}
// ToV returns the resource version.
func (g GVR) ToV() string {
tokens := strings.Split(string(g), "/")
return tokens[len(tokens)-2]
}
// ToR returns the resource name.
func (g GVR) ToR() string {
tokens := strings.Split(string(g), "/")
return tokens[len(tokens)-1]
}
// ToG returns the resource group name.
func (g GVR) ToG() string {
tokens := strings.Split(string(g), "/")
switch len(tokens) {
case 3:
return tokens[0]
default:
return ""
}
}

View File

@ -26,14 +26,6 @@ type RestMapper struct {
Connection
}
// Find a mapping given a resource kind.
func (*RestMapper) Find(kind string) (*meta.RESTMapping, error) {
if m, ok := kindToMapper[kind]; ok {
return m, nil
}
return nil, fmt.Errorf("no mapping for kind %s", kind)
}
// ToRESTMapper map resources to kind, and map kind and version to interfaces for manipulating K8s objects.
func (r *RestMapper) ToRESTMapper() (meta.RESTMapper, error) {
rc := r.RestConfigOrDie()
@ -115,154 +107,3 @@ func (*RestMapper) toRESTMapping(gvr schema.GroupVersionResource, res string) *m
func (*RestMapper) Name() meta.RESTScopeName {
return meta.RESTScopeNameNamespace
}
var kindToMapper = map[string]*meta.RESTMapping{
"ConfigMap": {
Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmap"},
GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"},
Scope: RestMapping,
},
"Pod": {
Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pod"},
GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"},
Scope: RestMapping,
},
"Service": {
Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "service"},
GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Service"},
Scope: RestMapping,
},
"EndPoints": {
Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "endpoints"},
GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Endpoints"},
Scope: RestMapping,
},
"Namespace": {
Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "namespace"},
GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"},
Scope: RestMapping,
},
"Node": {
Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "node"},
GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Node"},
Scope: RestMapping,
},
"PersistentVolume": {
Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "persistentvolume"},
GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "PersistentVolume"},
Scope: RestMapping,
},
"PersistentVolumeClaim": {
Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "persistentvolumeclaim"},
GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "PersistentVolumeClaim"},
Scope: RestMapping,
},
"ReplicationController": {
Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "replicationcontroller"},
GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ReplicationController"},
Scope: RestMapping,
},
"Secret": {
Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secret"},
GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"},
Scope: RestMapping,
},
"StorageClasse": {
Resource: schema.GroupVersionResource{Group: "storage.k8s.io", Version: "v1", Resource: "storageclass"},
GroupVersionKind: schema.GroupVersionKind{Group: "storage.k8s.io", Version: "v1", Kind: "StorageClass"},
Scope: RestMapping,
},
"ServiceAccount": {
Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "serviceaccount"},
GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ServiceAccount"},
Scope: RestMapping,
},
"Deployment": {
Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"},
GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
Scope: RestMapping,
},
"ReplicaSet": {
Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "replicaset"},
GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ReplicaSet"},
Scope: RestMapping,
},
"StatefulSet": {
Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "statefulsets"},
GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "StatefulSet"},
Scope: RestMapping,
},
"HorizontalPodAutoscaler": {
Resource: schema.GroupVersionResource{Group: "autoscaling", Version: "v1", Resource: "horizontalpodautoscaler"},
GroupVersionKind: schema.GroupVersionKind{Group: "autoscaling", Version: "v1", Kind: "HorizontalPodAutoscaler"},
Scope: RestMapping,
},
"Job": {
Resource: schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "job"},
GroupVersionKind: schema.GroupVersionKind{Group: "batch", Version: "v1", Kind: "Job"},
Scope: RestMapping,
},
"CronJob": {
Resource: schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "cronjob"},
GroupVersionKind: schema.GroupVersionKind{Group: "batch", Version: "v1", Kind: "CronJob"},
Scope: RestMapping,
},
"DaemonSet": {
Resource: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "daemonset"},
GroupVersionKind: schema.GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "DaemonSet"},
Scope: RestMapping,
},
"Ingress": {
Resource: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "ingress"},
GroupVersionKind: schema.GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "Ingress"},
Scope: RestMapping,
},
"ClusterRole": {
Resource: schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "clusterrole"},
GroupVersionKind: schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRole"},
Scope: RestMapping,
},
"ClusterRoleBinding": {
Resource: schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "clusterrolebinding"},
GroupVersionKind: schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding"},
Scope: RestMapping,
},
"Role": {
Resource: schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "role"},
GroupVersionKind: schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "Role"},
Scope: RestMapping,
},
"RoleBinding": {
Resource: schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "rolebinding"},
GroupVersionKind: schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBinding"},
Scope: RestMapping,
},
"CustomResourceDefinition": {
Resource: schema.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1beta1", Resource: "customresourcedefinitions"},
GroupVersionKind: schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1beta1", Kind: "CustomResourceDefinition"},
Scope: RestMapping,
},
"NetworkPolicy": {
Resource: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "networkpolicies"},
GroupVersionKind: schema.GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "NetworkPolicy"},
Scope: RestMapping,
},
"Event": {
Resource: schema.GroupVersionResource{Group: "events.k8s.io", Version: "v1beta1", Resource: "events"},
GroupVersionKind: schema.GroupVersionKind{Group: "events.k8s.io", Version: "v1beta1", Kind: "Event"},
Scope: RestMapping,
},
"PodDisruptionBudget": {
Resource: schema.GroupVersionResource{Group: "policy", Version: "v1beta1", Resource: "poddisruptionbudgets"},
GroupVersionKind: schema.GroupVersionKind{Group: "policy", Version: "v1beta1", Kind: "PodDisruptionBudget"},
Scope: RestMapping,
},
}

View File

@ -17,24 +17,24 @@ type Resource struct {
*base
Connection
group, version, name string
gvr GVR
}
// NewResource returns a new Resource.
func NewResource(c Connection, group, version, name string) *Resource {
return &Resource{base: &base{}, Connection: c, group: group, version: version, name: name}
func NewResource(c Connection, gvr GVR) *Resource {
return &Resource{base: &base{}, Connection: c, gvr: gvr}
}
// GetInfo returns info about apigroup.
func (r *Resource) GetInfo() (string, string, string) {
return r.group, r.version, r.name
func (r *Resource) GetInfo() GVR {
return r.gvr
}
func (r *Resource) nsRes() dynamic.NamespaceableResourceInterface {
g := schema.GroupVersionResource{
Group: r.group,
Version: r.version,
Resource: r.name,
Group: r.gvr.ToG(),
Version: r.gvr.ToV(),
Resource: r.gvr.ToR(),
}
return r.DynDialOrDie().Resource(g)
}
@ -46,7 +46,7 @@ func (r *Resource) Get(ns, n string) (interface{}, error) {
// List all Resources in a given namespace.
func (r *Resource) List(ns string) (Collection, error) {
obj, err := r.listAll(ns, r.name)
obj, err := r.listAll(ns, r.gvr.ToR())
if err != nil {
return nil, err
}
@ -82,9 +82,10 @@ func (r *Resource) listAll(ns, n string) (runtime.Object, error) {
func (r *Resource) getClient() (*rest.RESTClient, error) {
crConfig := r.RestConfigOrDie()
crConfig.GroupVersion = &schema.GroupVersion{Group: r.group, Version: r.version}
gv := r.gvr.AsGR()
crConfig.GroupVersion = &gv
crConfig.APIPath = "/apis"
if len(r.group) == 0 {
if len(r.gvr.ToG()) == 0 {
crConfig.APIPath = "/api"
}
codec, _ := r.codec()
@ -99,7 +100,7 @@ func (r *Resource) getClient() (*rest.RESTClient, error) {
func (r *Resource) codec() (serializer.CodecFactory, runtime.ParameterCodec) {
scheme := runtime.NewScheme()
gv := schema.GroupVersion{Group: r.group, Version: r.version}
gv := r.gvr.AsGR()
metav1.AddToGroupVersion(scheme, gv)
scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{})

View File

@ -3,14 +3,12 @@ package resource
import (
"bytes"
"context"
"fmt"
"path"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/watch"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
genericprinters "k8s.io/cli-runtime/pkg/printers"
@ -102,9 +100,7 @@ func (*Base) NumCols(n string) map[string]bool {
}
// ExtFields returns extended fields in relation to headers.
func (*Base) ExtFields() Properties {
return Properties{}
}
func (*Base) ExtFields(*TypeMeta) {}
// Get a resource by name
func (b *Base) Get(path string) (Columnar, error) {
@ -133,30 +129,13 @@ func (b *Base) List(ns string) (Columnars, error) {
}
// Describe a given resource.
func (b *Base) Describe(kind, pa string) (string, error) {
mapping, err := k8s.RestMapping.Find(kind)
if err == nil {
return b.doDescribe(pa, mapping)
}
resource, ok := b.Resource.(*k8s.Resource)
if !ok {
log.Debug().Msgf("resource not a (*k8s.Resource) and %s", err)
return "", fmt.Errorf("resource not a (*k8s.Resource) and %s", err)
}
g, v, n := resource.GetInfo()
func (b *Base) Describe(gvr, pa string) (string, error) {
mapper := k8s.RestMapper{Connection: b.Connection}
var e error
mapping, e = mapper.ResourceFor(fmt.Sprintf("%s.%s.%s", n, v, g))
if e != nil {
log.Debug().Err(e).Msgf("Unable to find mapper for %s %s", kind, pa)
return "", e
mapping, err := mapper.ResourceFor(k8s.GVR(gvr).ResName())
if err != nil {
log.Debug().Err(err).Msgf("Unable to find mapper for %s %s", gvr, pa)
return "", err
}
return b.doDescribe(pa, mapping)
}
func (b *Base) doDescribe(pa string, mapping *meta.RESTMapping) (string, error) {
ns, n := Namespaced(pa)
d, err := versioned.Describer(b.Connection.Config().Flags(), mapping)
if err != nil {

View File

@ -89,24 +89,33 @@ func (r *CustomResourceDefinition) Fields(ns string) Row {
}
// ExtFields returns extended fields.
func (r *CustomResourceDefinition) ExtFields() Properties {
var (
pp = Properties{}
i = r.instance
)
if spec, ok := i.Object["spec"].(map[string]interface{}); ok {
if meta, ok := i.Object["metadata"].(map[string]interface{}); ok {
pp["name"] = meta["name"]
}
pp["group"], pp["version"] = spec["group"], spec["version"]
if names, ok := spec["names"].(map[string]interface{}); ok {
pp["kind"] = names["kind"]
pp["singular"], pp["plural"] = names["singular"], names["plural"]
pp["aliases"] = names["shortNames"]
}
func (r *CustomResourceDefinition) ExtFields(m *TypeMeta) {
i := r.instance
spec, ok := i.Object["spec"].(map[string]interface{})
if !ok {
return
}
return pp
if meta, ok := i.Object["metadata"].(map[string]interface{}); ok {
m.Name = meta["name"].(string)
}
m.Group, m.Version = spec["group"].(string), spec["version"].(string)
m.Namespaced = isNamespaced(spec["scope"].(string))
names, ok := spec["names"].(map[string]interface{})
if !ok {
return
}
m.Kind = names["kind"].(string)
m.Singular, m.Plural = names["singular"].(string), names["plural"].(string)
if names["shortNames"] != nil {
for _, s := range names["shortNames"].([]interface{}) {
m.ShortNames = append(m.ShortNames, s.(string))
}
} else {
m.ShortNames = nil
}
}
func isNamespaced(scope string) bool {
return scope == "Namespaced"
}

View File

@ -43,12 +43,6 @@ func TestCRDFields(t *testing.T) {
assert.Equal(t, "fred", r[0])
}
func TestCRDExtFields(t *testing.T) {
p := newCRDFull().ExtFields()
assert.Equal(t, 7, len(p))
}
func TestCRDFieldsAllNS(t *testing.T) {
r := newCRD().Fields(resource.AllNamespaces)

View File

@ -17,35 +17,36 @@ import (
type Custom struct {
*Base
instance *metav1beta1.TableRow
group, version, name string
headers Row
instance *metav1beta1.TableRow
gvr k8s.GVR
headers Row
}
// NewCustomList returns a new resource list.
func NewCustomList(c k8s.Connection, ns, group, version, name string) List {
if !c.IsNamespaced(name) {
func NewCustomList(c k8s.Connection, namespaced bool, ns, gvr string) List {
if !namespaced {
ns = NotNamespaced
}
g := k8s.GVR(gvr)
return NewList(
ns,
name,
NewCustom(c, group, version, name), AllVerbsAccess|DescribeAccess,
g.ToR(),
NewCustom(c, g), AllVerbsAccess|DescribeAccess,
)
}
// NewCustom instantiates a new Kubernetes Resource.
func NewCustom(c k8s.Connection, group, version, name string) *Custom {
cr := &Custom{Base: &Base{Connection: c, Resource: k8s.NewResource(c, group, version, name)}}
func NewCustom(c k8s.Connection, gvr k8s.GVR) *Custom {
cr := &Custom{Base: &Base{Connection: c, Resource: k8s.NewResource(c, gvr)}}
cr.Factory = cr
cr.group, cr.version, cr.name = group, version, name
cr.gvr = gvr
return cr
}
// New builds a new Custom instance from a k8s resource.
func (r *Custom) New(i interface{}) Columnar {
cr := NewCustom(r.Connection, "", "", "")
cr := NewCustom(r.Connection, "")
switch instance := i.(type) {
case *metav1beta1.TableRow:
cr.instance = instance
@ -66,7 +67,7 @@ func (r *Custom) New(i interface{}) Columnar {
}
name := meta["name"].(string)
cr.path = path.Join(ns, name)
cr.group, cr.version, cr.name = obj["kind"].(string), obj["apiVersion"].(string), name
cr.gvr = k8s.NewGVR(obj["kind"].(string), obj["apiVersion"].(string), name)
return cr
}

View File

@ -130,7 +130,7 @@ func k8sCustomRow() *metav1beta1.TableRow {
func newCustom() resource.Columnar {
mc := NewMockConnection()
return resource.NewCustom(mc, "g", "v1", "fred").New(k8sCustomRow())
return resource.NewCustom(mc, "g/v1/fred").New(k8sCustomRow())
}
func customYaml() string {

View File

@ -48,8 +48,17 @@ type (
// RowEvents tracks resource update events.
RowEvents map[string]*RowEvent
// Properties a collection of extra properties on a K8s resource.
Properties map[string]interface{}
// TypeMeta represents resource type meta data.
TypeMeta struct {
Name string
Namespaced bool
Group string
Version string
Kind string
Singular string
Plural string
ShortNames []string
}
// TableData tracks a K8s resource for tabular display.
TableData struct {
@ -81,7 +90,7 @@ type (
Columnar interface {
Header(ns string) Row
Fields(ns string) Row
ExtFields() Properties
ExtFields(*TypeMeta)
Name() string
SetPodMetrics(*mv1beta1.PodMetrics)
SetNodeMetrics(*mv1beta1.NodeMetrics)
@ -102,7 +111,7 @@ type (
Get(path string) (Columnar, error)
List(ns string) (Columnars, error)
Delete(path string, cascade, force bool) error
Describe(kind, pa string) (string, error)
Describe(gvr, pa string) (string, error)
Marshal(pa string) (string, error)
Header(ns string) Row
NumCols(ns string) map[string]bool

View File

@ -123,7 +123,7 @@ func stsHeader() resource.Row {
}
func stsYaml() string {
return `apiVersion: v1
return `apiVersion: apps/v1
kind: StatefulSet
metadata:
creationTimestamp: "2018-12-14T17:36:43Z"

View File

@ -30,19 +30,21 @@ func NewKeyAction(d string, a ActionHandler, display bool) KeyAction {
// Hints returns a collection of hints.
func (a KeyActions) Hints() Hints {
kk := make([]int, 0, len(a))
for k, v := range a {
if v.Visible {
kk = append(kk, int(k))
}
for k := range a {
kk = append(kk, int(k))
}
sort.Ints(kk)
hh := make(Hints, 0, len(kk))
for _, k := range kk {
if name, ok := tcell.KeyNames[tcell.Key(k)]; ok {
hh = append(hh, Hint{
Mnemonic: name,
Description: a[tcell.Key(k)].Description})
hh = append(hh,
Hint{
Mnemonic: name,
Description: a[tcell.Key(k)].Description,
Visible: a[tcell.Key(k)].Visible,
},
)
} else {
log.Error().Msgf("Unable to locate KeyName for %#v", string(k))
}

View File

@ -7,7 +7,6 @@ import (
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
)
// Igniter represents an initializable view.
@ -125,8 +124,8 @@ func (a *App) GetCmd() string {
return a.cmdBuff.String()
}
// GetCmdBuff returns a cmd buffer.
func (a *App) GetCmdBuff() *CmdBuff {
// CmdBuff returns a cmd buffer.
func (a *App) CmdBuff() *CmdBuff {
return a.cmdBuff
}
@ -177,7 +176,6 @@ func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey {
}
if a, ok := a.actions[key]; ok {
log.Debug().Msgf(">> App handled key: %s", tcell.KeyNames[key])
return a.Action(evt)
}

View File

@ -61,11 +61,13 @@ func (v *CmdView) write(s string) {
// ----------------------------------------------------------------------------
// Event Listener protocol...
func (v *CmdView) changed(s string) {
// BufferChanged indicates the buffer was changed.
func (v *CmdView) BufferChanged(s string) {
v.update(s)
}
func (v *CmdView) active(f bool) {
// BufferActive indicates the buff activity changed.
func (v *CmdView) BufferActive(f bool) {
v.activated = f
if f {
v.SetBorder(true)

View File

@ -3,9 +3,13 @@ package ui
const maxBuff = 10
type (
buffWatcher interface {
changed(s string)
active(state bool)
// BuffWatcher represents a command buffer listener.
BuffWatcher interface {
// Changed indicates the buffer was changed.
BufferChanged(s string)
// Active indicates the buff activity changed.
BufferActive(state bool)
}
// CmdBuff represents user command input.
@ -13,7 +17,7 @@ type (
buff []rune
hotKey rune
active bool
listeners []buffWatcher
listeners []BuffWatcher
}
)
@ -22,7 +26,7 @@ func NewCmdBuff(key rune) *CmdBuff {
return &CmdBuff{
hotKey: key,
buff: make([]rune, 0, maxBuff),
listeners: []buffWatcher{},
listeners: []BuffWatcher{},
}
}
@ -88,18 +92,18 @@ func (c *CmdBuff) Empty() bool {
// Event Listeners...
// AddListener registers a cmd buffer listener.
func (c *CmdBuff) AddListener(w ...buffWatcher) {
func (c *CmdBuff) AddListener(w ...BuffWatcher) {
c.listeners = append(c.listeners, w...)
}
func (c *CmdBuff) fireChanged() {
for _, l := range c.listeners {
l.changed(c.String())
l.BufferChanged(c.String())
}
}
func (c *CmdBuff) fireActive(b bool) {
for _, l := range c.listeners {
l.active(b)
l.BufferActive(b)
}
}

View File

@ -12,11 +12,11 @@ type testListener struct {
inact int
}
func (l *testListener) changed(s string) {
func (l *testListener) BufferChanged(s string) {
l.text = s
}
func (l *testListener) active(s bool) {
func (l *testListener) BufferActive(s bool) {
if s {
l.act++
return

View File

@ -23,6 +23,6 @@ func TestCmdInCmdMode(t *testing.T) {
assert.Equal(t, "T> blee!\n", v.GetText(false))
assert.False(t, v.InCmdMode())
v.active(true)
v.BufferActive(true)
assert.True(t, v.InCmdMode())
}

View File

@ -8,7 +8,9 @@ import (
type (
// Hint represents keyboard mnemonic.
Hint struct {
Mnemonic, Description string
Mnemonic string
Description string
Visible bool
}
// Hints a collection of keyboard mnemonics.
Hints []Hint

View File

@ -13,14 +13,14 @@ import (
// IndicatorView represents a status indicator.
type IndicatorView struct {
*tview.TextView
app *App
styles *config.Styles
permanent string
cancel context.CancelFunc
cancel context.CancelFunc
}
// NewIndicatorView returns a new logo.
// NewIndicatorView returns a new status indicator.
func NewIndicatorView(app *App, styles *config.Styles) *IndicatorView {
v := IndicatorView{
TextView: tview.NewTextView(),

View File

@ -69,6 +69,9 @@ func (v *MenuView) buildMenuTable(hh Hints) [][]string {
firstCmd := true
maxKeys := make([]int, colCount+1)
for _, h := range hh {
if !h.Visible {
continue
}
isDigit := menuRX.MatchString(h.Mnemonic)
if !isDigit && firstCmd {
row, col, firstCmd = 0, col+1, false

View File

@ -12,9 +12,9 @@ func TestNewMenuView(t *testing.T) {
defaults, _ := config.NewStyles("")
v := NewMenuView(defaults)
v.HydrateMenu(Hints{
{"a", "bleeA"},
{"b", "bleeB"},
{"0", "zero"},
{"a", "bleeA", true},
{"b", "bleeB", true},
{"0", "zero", true},
})
assert.Equal(t, " [fuchsia:black:b]<0> [white:black:d]zero ", v.GetCell(0, 0).Text)
@ -35,9 +35,10 @@ func TestKeyActions(t *testing.T) {
tcell.Key(Key1): NewKeyAction("one", nil, false),
},
e: Hints{
{"0", "zero"},
{"a", "bleeA"},
{"b", "bleeB"},
{"0", "zero", true},
{"1", "one", false},
{"a", "bleeA", true},
{"b", "bleeB", true},
},
},
}

View File

@ -174,8 +174,8 @@ func (v *Table) GetSelectedItem() string {
func (v *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
key := evt.Key()
if key == tcell.KeyRune {
if v.Cmd().IsActive() {
v.Cmd().Add(evt.Rune())
if v.SearchBuff().IsActive() {
v.SearchBuff().Add(evt.Rune())
v.ClearSelection()
v.doUpdate(v.filtered())
v.SelectFirstRow()
@ -185,7 +185,6 @@ func (v *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
}
if a, ok := v.actions[key]; ok {
log.Debug().Msgf(">> TableView handled %s", tcell.KeyNames[key])
return a.Action(evt)
}
@ -242,7 +241,7 @@ func (v *Table) Update(data resource.TableData) {
func (v *Table) doUpdate(data resource.TableData) {
v.activeNS = data.Namespace
if v.activeNS == resource.AllNamespaces && v.activeNS != "*" {
v.actions[KeyShiftP] = NewKeyAction("Sort Namespace", v.SortColCmd(-2), true)
v.actions[KeyShiftP] = NewKeyAction("Sort Namespace", v.SortColCmd(-2), false)
} else {
delete(v.actions, KeyShiftP)
}
@ -438,8 +437,8 @@ func (v *Table) KeyBindings() KeyActions {
return v.actions
}
// Cmd returns the associated command buffer.
func (v *Table) Cmd() *CmdBuff {
// SearchBuff returns the associated command buffer.
func (v *Table) SearchBuff() *CmdBuff {
return v.cmdBuff
}

View File

@ -3,7 +3,9 @@ package views
import (
"context"
"fmt"
"strings"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/resource"
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
@ -11,7 +13,7 @@ import (
const (
aliasTitle = "Aliases"
aliasTitleFmt = " [aqua::b]%s([fuchsia::b]%d[fuchsia::-])[aqua::-] "
aliasTitleFmt = " [aqua::b]%s([fuchsia::b]%d[fuchsia::-][aqua::-]) "
)
type aliasView struct {
@ -52,8 +54,8 @@ func (v *aliasView) registerActions() {
tcell.KeyEnter: ui.NewKeyAction("Goto", v.gotoCmd, true),
tcell.KeyEscape: ui.NewKeyAction("Reset", v.resetCmd, false),
ui.KeySlash: ui.NewKeyAction("Filter", v.activateCmd, false),
ui.KeyShiftR: ui.NewKeyAction("Sort Resources", v.SortColCmd(1), true),
ui.KeyShiftO: ui.NewKeyAction("Sort Groups", v.SortColCmd(2), true),
ui.KeyShiftR: ui.NewKeyAction("Sort Resources", v.SortColCmd(1), false),
ui.KeyShiftO: ui.NewKeyAction("Sort Groups", v.SortColCmd(2), false),
})
}
@ -62,8 +64,8 @@ func (v *aliasView) getTitle() string {
}
func (v *aliasView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.Cmd().Empty() {
v.Cmd().Reset()
if !v.SearchBuff().Empty() {
v.SearchBuff().Reset()
return nil
}
@ -73,13 +75,15 @@ func (v *aliasView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
func (v *aliasView) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
r, _ := v.GetSelection()
if r != 0 {
return v.runCmd(evt)
s := ui.TrimCell(v.Table, r, 1)
tokens := strings.Split(s, ",")
v.app.gotoResource(tokens[0], true)
return nil
}
if v.Cmd().IsActive() {
if v.SearchBuff().IsActive() {
return v.activateCmd(evt)
}
return evt
}
@ -88,8 +92,8 @@ func (v *aliasView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
v.cancel()
}
if v.Cmd().IsActive() {
v.Cmd().Reset()
if v.SearchBuff().IsActive() {
v.SearchBuff().Reset()
} else {
v.app.inject(v.current)
}
@ -97,32 +101,30 @@ func (v *aliasView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
func (v *aliasView) runCmd(evt *tcell.EventKey) *tcell.EventKey {
r, _ := v.GetSelection()
if r > 0 {
v.app.gotoResource(ui.TrimCell(v.Table, r, 0), true)
}
return nil
}
func (v *aliasView) hydrate() resource.TableData {
cmds := make(map[string]*resCmd, 40)
aliasCmds(v.app.Conn(), cmds)
data := resource.TableData{
Header: resource.Row{"ALIAS", "RESOURCE", "APIGROUP"},
Rows: make(resource.RowEvents, len(cmds)),
Header: resource.Row{"RESOURCE", "COMMAND", "APIGROUP"},
Rows: make(resource.RowEvents, len(aliases.Alias)),
Namespace: resource.NotNamespaced,
}
for k := range cmds {
fields := resource.Row{
ui.Pad(k, 30),
ui.Pad(cmds[k].gvr, 30),
ui.Pad(cmds[k].api, 30),
aa := make(map[string][]string, len(aliases.Alias))
for alias, gvr := range aliases.Alias {
if _, ok := aa[gvr]; ok {
aa[gvr] = append(aa[gvr], alias)
} else {
aa[gvr] = []string{alias}
}
data.Rows[k] = &resource.RowEvent{
}
for gvr, aliases := range aa {
g := k8s.GVR(gvr)
fields := resource.Row{
ui.Pad(g.ToR(), 30),
ui.Pad(strings.Join(aliases, ","), 70),
ui.Pad(g.ToG(), 30),
}
data.Rows[string(gvr)] = &resource.RowEvent{
Action: resource.New,
Fields: fields,
Deltas: fields,

View File

@ -13,6 +13,6 @@ func TestAliasView(t *testing.T) {
v.Init(nil, "")
assert.Equal(t, 3, len(td.Header))
assert.Equal(t, 41, len(td.Rows))
assert.Equal(t, 15, len(td.Rows))
assert.Equal(t, "Aliases", v.getTitle())
}

View File

@ -54,6 +54,7 @@ type (
stopCh chan struct{}
forwarders map[string]forwarder
version string
showHeader bool
}
)
@ -76,11 +77,13 @@ func NewApp(cfg *config.Config) *appView {
func (a *appView) Init(version string, rate int) {
a.version = version
a.CmdBuff().AddListener(a)
a.App.Init()
a.AddActions(ui.KeyActions{
tcell.KeyCtrlH: ui.NewKeyAction("ToggleHeader", a.toggleHeaderCmd, false),
ui.KeyHelp: ui.NewKeyAction("Help", a.helpCmd, false),
tcell.KeyCtrlA: ui.NewKeyAction("Aliases", a.aliasCmd, true),
tcell.KeyCtrlA: ui.NewKeyAction("Aliases", a.aliasCmd, false),
tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, false),
})
@ -96,29 +99,60 @@ func (a *appView) Init(version string, rate int) {
}
}
header := tview.NewFlex()
{
header.SetDirection(tview.FlexColumn)
header.AddItem(a.clusterInfo(), 35, 1, false)
header.AddItem(a.Menu(), 0, 1, false)
header.AddItem(a.Logo(), 26, 1, false)
}
main := tview.NewFlex()
main.SetDirection(tview.FlexRow)
a.Main().AddPage("main", main, true, false)
a.Main().AddPage("splash", ui.NewSplash(a.Styles, version), true, true)
if !a.Config.K9s.GetHeadless() {
main.AddItem(header, 7, 1, false)
} else {
main.AddItem(a.indicator(), 1, 1, false)
}
main.AddItem(a.Cmd(), 3, 1, false)
main.AddItem(a.indicator(), 1, 1, false)
// main.AddItem(a.Cmd(), 3, 1, false)
main.AddItem(a.Frame(), 0, 10, true)
main.AddItem(a.Crumbs(), 2, 1, false)
main.AddItem(a.Flash(), 1, 1, false)
a.toggleHeader(!a.Config.K9s.GetHeadless())
}
a.Main().AddPage("main", main, true, false)
a.Main().AddPage("splash", ui.NewSplash(a.Styles, version), true, true)
// Changed indicates the buffer was changed.
func (a *appView) BufferChanged(s string) {}
// Active indicates the buff activity changed.
func (a *appView) BufferActive(state bool) {
flex, ok := a.Main().GetPrimitive("main").(*tview.Flex)
if !ok {
return
}
if state {
flex.AddItemAtIndex(1, a.Cmd(), 3, 1, false)
} else if flex.ItemAt(1) == a.Cmd() {
flex.RemoveItemAtIndex(1)
}
a.Draw()
}
func (a *appView) toggleHeader(flag bool) {
a.showHeader = flag
flex := a.Main().GetPrimitive("main").(*tview.Flex)
if a.showHeader {
flex.RemoveItemAtIndex(0)
flex.AddItemAtIndex(0, a.buildHeader(), 7, 1, false)
} else {
flex.RemoveItemAtIndex(0)
flex.AddItemAtIndex(0, a.indicator(), 1, 1, false)
a.refreshIndicator()
}
}
func (a *appView) buildHeader() tview.Primitive {
header := tview.NewFlex()
header.SetDirection(tview.FlexColumn)
if !a.showHeader {
return header
}
header.AddItem(a.clusterInfo(), 35, 1, false)
header.AddItem(a.Menu(), 0, 1, false)
header.AddItem(a.Logo(), 26, 1, false)
return header
}
func (a *appView) clusterUpdater(ctx context.Context) {
@ -129,10 +163,11 @@ func (a *appView) clusterUpdater(ctx context.Context) {
return
case <-time.After(clusterRefresh):
a.QueueUpdateDraw(func() {
if a.Config.K9s.GetHeadless() {
if !a.showHeader {
a.refreshIndicator()
} else {
a.clusterInfo().refresh()
}
a.clusterInfo().refresh()
})
}
}
@ -273,6 +308,14 @@ func (a *appView) setIndicator(l ui.FlashLevel, msg string) {
a.Draw()
}
func (a *appView) toggleHeaderCmd(evt *tcell.EventKey) *tcell.EventKey {
a.showHeader = !a.showHeader
a.toggleHeader(a.showHeader)
a.Draw()
return nil
}
func (a *appView) prevCmd(evt *tcell.EventKey) *tcell.EventKey {
if top, ok := a.command.previousCmd(); ok {
log.Debug().Msgf("Previous command %s", top)
@ -283,7 +326,7 @@ func (a *appView) prevCmd(evt *tcell.EventKey) *tcell.EventKey {
}
func (a *appView) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.GetCmdBuff().IsActive() && !a.GetCmdBuff().Empty() {
if a.CmdBuff().IsActive() && !a.CmdBuff().Empty() {
a.gotoResource(a.GetCmd(), true)
a.ResetCmd()
return nil
@ -294,9 +337,6 @@ func (a *appView) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
}
func (a *appView) helpCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.InCmdMode() {
return evt
}
if _, ok := a.Frame().GetPrimitive("main").(*helpView); ok {
return evt
}
@ -307,9 +347,6 @@ func (a *appView) helpCmd(evt *tcell.EventKey) *tcell.EventKey {
}
func (a *appView) aliasCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.InCmdMode() {
return evt
}
if _, ok := a.Frame().GetPrimitive("main").(*aliasView); ok {
return evt
}
@ -337,10 +374,8 @@ func (a *appView) inject(i ui.Igniter) {
}
a.Frame().RemovePage("main")
var ctx context.Context
{
ctx, a.cancel = context.WithCancel(context.Background())
i.Init(ctx, a.Config.ActiveNamespace())
}
ctx, a.cancel = context.WithCancel(context.Background())
i.Init(ctx, a.Config.ActiveNamespace())
a.Frame().AddPage("main", i, true, true)
a.SetFocus(i)
}

View File

@ -40,7 +40,7 @@ type benchView struct {
app *appView
}
func newBenchView(title string, app *appView, _ resource.List) resourceViewer {
func newBenchView(title, gvr string, app *appView, _ resource.List) resourceViewer {
v := benchView{app: app}
v.masterDetail = newMasterDetail(benchTitle, "", app, v.backCmd)
v.keyBindings()
@ -108,7 +108,7 @@ func (v *benchView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tce
}
func (v *benchView) enterCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.masterPage().Cmd().IsActive() {
if v.masterPage().SearchBuff().IsActive() {
return v.masterPage().filterCmd(evt)
}

View File

@ -1,8 +1,11 @@
package views
import (
"fmt"
"regexp"
"time"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/resource"
"github.com/derailed/k9s/internal/ui"
"github.com/rs/zerolog/log"
@ -41,15 +44,18 @@ func (c *command) previousCmd() (string, bool) {
// DefaultCmd reset default command ie show pods.
func (c *command) defaultCmd() {
c.pushCmd(c.app.Config.ActiveView())
c.run(c.app.Config.ActiveView())
cmd := c.app.Config.ActiveView()
c.pushCmd(cmd)
if !c.run(cmd) {
log.Error().Err(fmt.Errorf("Unable to load command %s", cmd)).Msg("Command failed")
}
}
// Helpers...
var policyMatcher = regexp.MustCompile(`\Apol\s([u|g|s]):([\w-:]+)\b`)
func (c *command) isStdCmd(cmd string) bool {
func (c *command) isCustCmd(cmd string) bool {
switch {
case cmd == "q", cmd == "quit":
c.app.BailOut()
@ -70,87 +76,72 @@ func (c *command) isStdCmd(cmd string) bool {
return false
}
func (c *command) isAliasCmd(alias string, cmds map[string]resCmd) bool {
res, ok := cmds[alias]
if !ok {
return false
}
var r resource.List
if res.listFn != nil {
r = res.listFn(c.app.Conn(), resource.DefaultNamespace)
}
v := newResourceView(res.gvr, c.app, r)
if res.viewFn != nil {
v = res.viewFn(res.gvr, c.app, r)
}
if res.colorerFn != nil {
v.setColorerFn(res.colorerFn)
}
if res.enterFn != nil {
v.setEnterFn(res.enterFn)
}
if res.decorateFn != nil {
v.setDecorateFn(res.decorateFn)
}
const fmat = "Viewing resource %s..."
c.app.Flash().Infof(fmat, res.gvr)
log.Debug().Msgf("Running command %s", alias)
c.exec(alias, v)
return true
}
func (c *command) isCRDCmd(cmd string) bool {
crds := map[string]resCmd{}
res, ok := crds[cmd]
if !ok {
return false
}
name := res.plural
if name == "" {
name = res.singular
}
v := newResourceView(
res.gvr,
c.app,
resource.NewCustomList(c.app.Conn(), "", res.api, res.version, name),
)
v.setColorerFn(ui.DefaultColorer)
c.exec(cmd, v)
return true
}
// Exec the command by showing associated display.
func (c *command) run(cmd string) bool {
if c.isStdCmd(cmd) {
defer func(t time.Time) {
log.Debug().Msgf("RUN CMD Elapsed %v", time.Since(t))
}(time.Now())
if c.isCustCmd(cmd) {
return true
}
cmds := make(map[string]resCmd, 30)
resourceViews(c.app.Conn(), cmds)
allCRDs(c.app.Conn(), cmds)
a, ok := aliases[cmd]
vv := make(viewers, 200)
resourceViews(c.app.Conn(), vv)
allCRDs(c.app.Conn(), vv)
gvr, ok := aliases.Get(cmd)
if !ok {
log.Error().Err(fmt.Errorf("Huh? `%s` command not found", cmd)).Msg("Command Failed")
c.app.Flash().Warnf("Huh? `%s` command not found", cmd)
return false
}
return c.isAliasCmd(a, cmds)
v, ok := vv[gvr]
if !ok {
log.Error().Err(fmt.Errorf("Huh? `%s` viewer not found", cmd)).Msg("Viewer Failed")
c.app.Flash().Warnf("Huh? `%s` viewer not found", gvr)
return false
}
return c.execCmd(gvr, v)
}
func (c *command) exec(cmd string, v ui.Igniter) {
if v == nil {
return
func (c *command) execCmd(gvr string, v viewer) bool {
log.Debug().Msgf("ExecCmd gvr %s", gvr)
var r resource.List
if v.listFn != nil {
r = v.listFn(c.app.Conn(), resource.DefaultNamespace)
}
c.app.Config.SetActiveView(cmd)
var view resourceViewer
if v.viewFn != nil {
view = v.viewFn(v.kind, gvr, c.app, r)
} else {
view = newResourceView(v.kind, gvr, c.app, r)
}
if v.colorerFn != nil {
view.setColorerFn(v.colorerFn)
}
if v.enterFn != nil {
view.setEnterFn(v.enterFn)
}
if v.decorateFn != nil {
view.setDecorateFn(v.decorateFn)
}
return c.exec(gvr, view)
}
func (c *command) exec(gvr string, v ui.Igniter) bool {
if v == nil {
log.Error().Err(fmt.Errorf("No igniter given for %s", gvr))
return false
}
g := k8s.GVR(gvr)
c.app.Flash().Infof("Viewing resource %s...", g.ToR())
log.Debug().Msgf("Running command %s", gvr)
c.app.Config.SetActiveView(g.ToR())
c.app.Config.Save()
c.app.inject(v)
return true
}

View File

@ -22,8 +22,8 @@ type containerView struct {
exitFn func()
}
func newContainerView(ns string, app *appView, list resource.List, path string, exitFn func()) resourceViewer {
v := containerView{logResourceView: newLogResourceView(ns, app, list)}
func newContainerView(title string, app *appView, list resource.List, path string, exitFn func()) resourceViewer {
v := containerView{logResourceView: newLogResourceView(title, "", app, list)}
v.path = &path
v.envFn = v.k9sEnv
v.containerFn = v.selectedContainer
@ -48,10 +48,10 @@ func (v *containerView) extraActions(aa ui.KeyActions) {
aa[ui.KeyS] = ui.NewKeyAction("Shell", v.shellCmd, true)
aa[tcell.KeyEscape] = ui.NewKeyAction("Back", v.backCmd, false)
aa[ui.KeyP] = ui.NewKeyAction("Previous", v.backCmd, false)
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", v.sortColCmd(6, false), true)
aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", v.sortColCmd(7, false), true)
aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", v.sortColCmd(8, false), true)
aa[ui.KeyShiftZ] = ui.NewKeyAction("Sort MEM%", v.sortColCmd(9, false), true)
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", v.sortColCmd(6, false), false)
aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", v.sortColCmd(7, false), false)
aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", v.sortColCmd(8, false), false)
aa[ui.KeyShiftZ] = ui.NewKeyAction("Sort MEM%", v.sortColCmd(9, false), false)
}
func (v *containerView) k9sEnv() K9sEnv {

View File

@ -12,8 +12,8 @@ type contextView struct {
*resourceView
}
func newContextView(title string, app *appView, list resource.List) resourceViewer {
v := contextView{newResourceView(title, app, list).(*resourceView)}
func newContextView(title, gvr string, app *appView, list resource.List) resourceViewer {
v := contextView{newResourceView(title, gvr, app, list).(*resourceView)}
v.extraActionsFn = v.extraActions
v.enterFn = v.useCtx
v.masterPage().SetSelectedFn(v.cleanser)

View File

@ -12,7 +12,7 @@ func TestContextView(t *testing.T) {
l := resource.NewContextList(nil, "fred")
v := newContextView("blee", NewApp(config.NewConfig(ks{})), l).(*contextView)
assert.Equal(t, 3, len(v.hints()))
assert.Equal(t, 10, len(v.hints()))
}
func TestCleaner(t *testing.T) {

View File

@ -10,8 +10,8 @@ type cronJobView struct {
*resourceView
}
func newCronJobView(t string, app *appView, list resource.List) resourceViewer {
v := cronJobView{resourceView: newResourceView(t, app, list).(*resourceView)}
func newCronJobView(title, gvr string, app *appView, list resource.List) resourceViewer {
v := cronJobView{resourceView: newResourceView(title, gvr, app, list).(*resourceView)}
v.extraActionsFn = v.extraActions
return &v

View File

@ -15,8 +15,8 @@ type deployView struct {
const scaleDialogKey = "scale"
func newDeployView(title string, app *appView, list resource.List) resourceViewer {
logResourceView := newLogResourceView(title, app, list)
func newDeployView(title, gvr string, app *appView, list resource.List) resourceViewer {
logResourceView := newLogResourceView(title, gvr, app, list)
v := deployView{logResourceView, newScalableResourceViewForParent(logResourceView.resourceView)}
v.extraActionsFn = v.extraActions
v.enterFn = v.showPods
@ -27,8 +27,8 @@ func newDeployView(title string, app *appView, list resource.List) resourceViewe
func (v *deployView) extraActions(aa ui.KeyActions) {
v.logResourceView.extraActions(aa)
v.scalableResourceView.extraActions(aa)
aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(2, false), true)
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(3, false), true)
aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(2, false), false)
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(3, false), false)
}
func (v *deployView) showPods(app *appView, _, res, sel string) {

View File

@ -12,5 +12,5 @@ func TestDeployView(t *testing.T) {
l := resource.NewDeploymentList(nil, "fred")
v := newDeployView("blee", NewApp(config.NewConfig(ks{})), l).(*deployView)
assert.Equal(t, 3, len(v.hints()))
assert.Equal(t, 10, len(v.hints()))
}

View File

@ -12,8 +12,8 @@ type daemonSetView struct {
*logResourceView
}
func newDaemonSetView(t string, app *appView, list resource.List) resourceViewer {
v := daemonSetView{newLogResourceView(t, app, list)}
func newDaemonSetView(title, gvr string, app *appView, list resource.List) resourceViewer {
v := daemonSetView{newLogResourceView(title, gvr, app, list)}
v.extraActionsFn = v.extraActions
v.enterFn = v.showPods
@ -22,8 +22,8 @@ func newDaemonSetView(t string, app *appView, list resource.List) resourceViewer
func (v *daemonSetView) extraActions(aa ui.KeyActions) {
v.logResourceView.extraActions(aa)
aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(2, false), true)
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(3, false), true)
aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(2, false), false)
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(3, false), false)
}
func (v *daemonSetView) showPods(app *appView, _, res, sel string) {

View File

@ -12,5 +12,5 @@ func TestDaemonSetView(t *testing.T) {
l := resource.NewDaemonSetList(nil, "fred")
v := newDaemonSetView("blee", NewApp(config.NewConfig(ks{})), l).(*daemonSetView)
assert.Equal(t, 3, len(v.hints()))
assert.Equal(t, 10, len(v.hints()))
}

View File

@ -34,7 +34,7 @@ type dumpView struct {
cancel context.CancelFunc
}
func newDumpView(_ string, app *appView, _ resource.List) resourceViewer {
func newDumpView(_, _ string, app *appView, _ resource.List) resourceViewer {
v := dumpView{
Pages: tview.NewPages(),
app: app,
@ -112,7 +112,7 @@ func (v *dumpView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcel
func (v *dumpView) enterCmd(evt *tcell.EventKey) *tcell.EventKey {
log.Debug().Msg("Dump enter!")
tv := v.getTV()
if tv.Cmd().IsActive() {
if tv.SearchBuff().IsActive() {
return tv.filterCmd(evt)
}
sel := tv.GetSelectedItem()

View File

@ -33,7 +33,7 @@ type forwardView struct {
var _ resourceViewer = &forwardView{}
func newForwardView(ns string, app *appView, list resource.List) resourceViewer {
func newForwardView(title, gvr string, app *appView, list resource.List) resourceViewer {
v := forwardView{
Pages: tview.NewPages(),
app: app,
@ -107,8 +107,8 @@ func (v *forwardView) registerActions() {
tcell.KeyCtrlD: ui.NewKeyAction("Delete", v.deleteCmd, true),
ui.KeySlash: ui.NewKeyAction("Filter", tv.activateCmd, false),
ui.KeyP: ui.NewKeyAction("Previous", v.app.prevCmd, false),
ui.KeyShiftP: ui.NewKeyAction("Sort Ports", v.sortColCmd(2, true), true),
ui.KeyShiftU: ui.NewKeyAction("Sort URL", v.sortColCmd(4, true), true),
ui.KeyShiftP: ui.NewKeyAction("Sort Ports", v.sortColCmd(2, true), false),
ui.KeyShiftU: ui.NewKeyAction("Sort URL", v.sortColCmd(4, true), false),
})
}
@ -210,8 +210,8 @@ func (v *forwardView) getSelectedItem() string {
func (v *forwardView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
tv := v.getTV()
if !tv.Cmd().Empty() {
tv.Cmd().Reset()
if !tv.SearchBuff().Empty() {
tv.SearchBuff().Reset()
return nil
}
@ -243,8 +243,8 @@ func (v *forwardView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
}
tv := v.getTV()
if tv.Cmd().IsActive() {
tv.Cmd().Reset()
if tv.SearchBuff().IsActive() {
tv.SearchBuff().Reset()
} else {
v.app.inject(v.app.Frame().GetPrimitive("main").(ui.Igniter))
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
"runtime"
"sort"
"strconv"
"strings"
"github.com/derailed/k9s/internal/ui"
@ -80,35 +81,95 @@ func (v *helpView) Init(_ context.Context, _ string) {
func (v *helpView) showHelp() ui.Hints {
return ui.Hints{
{"?", "Help"},
{"Ctrl-a", "Aliases view"},
{
Mnemonic: "?",
Description: "Help",
},
{
Mnemonic: "Ctrl-a",
Description: "Aliases",
},
}
}
func (v *helpView) showNav() ui.Hints {
return ui.Hints{
{"g", "Goto Top"},
{"Shift-g", "Goto Bottom"},
{"Ctrl-b", "Page Down"},
{"Ctrl-f", "Page Up"},
{"h", "Left"},
{"l", "Right"},
{"k", "Up"},
{"j", "Down"},
{
Mnemonic: "g",
Description: "Goto Top",
},
{
Mnemonic: "Shift-g",
Description: "Goto Bottom",
},
{
Mnemonic: "Ctrl-b",
Description: "Page Down"},
{
Mnemonic: "Ctrl-f",
Description: "Page Up",
},
{
Mnemonic: "h",
Description: "Left",
},
{
Mnemonic: "l",
Description: "Right",
},
{
Mnemonic: "k",
Description: "Up",
},
{
Mnemonic: "j",
Description: "Down",
},
}
}
func (v *helpView) showGeneral() ui.Hints {
return ui.Hints{
{":cmd", "Command mode"},
{"/term", "Filter mode"},
{"esc", "Clear filter"},
{"tab", "Next term match"},
{"backtab", "Previous term match"},
{"Ctrl-r", "Refresh"},
{"Shift-i", "Invert Sort"},
{"p", "Previous resource view"},
{":q", "Quit"},
{
Mnemonic: ":cmd",
Description: "Command mode",
},
{
Mnemonic: "/term",
Description: "Filter mode",
},
{
Mnemonic: "esc",
Description: "Clear filter",
},
{
Mnemonic: "tab",
Description: "Next Field",
},
{
Mnemonic: "backtab",
Description: "Previous Field",
},
{
Mnemonic: "Ctrl-r",
Description: "Refresh",
},
{
Mnemonic: "Ctrl-h",
Description: "Toggle Header",
},
{
Mnemonic: "Shift-i",
Description: "Invert Sort",
},
{
Mnemonic: "p",
Description: "Previous View",
},
{
Mnemonic: ":q",
Description: "Quit",
},
}
}
@ -127,24 +188,30 @@ func (v *helpView) resetTitle() {
func (v *helpView) build(hh ui.Hints) {
v.Clear()
sort.Sort(hh)
v.addSection(0, 0, "Resource", hh)
v.addSection(0, 4, "General", v.showGeneral())
v.addSection(0, 6, "Navigation", v.showNav())
v.addSection(0, 8, "Help", v.showHelp())
v.addSection(0, 0, "RESOURCE", hh)
v.addSection(0, 4, "GENERAL", v.showGeneral())
v.addSection(0, 6, "NAVIGATION", v.showNav())
v.addSection(0, 8, "HELP", v.showHelp())
}
func (v *helpView) addSection(r, c int, title string, hh ui.Hints) {
row := r
cell := tview.NewTableCell(title)
cell.SetTextColor(tcell.ColorWhite)
cell.SetTextColor(tcell.ColorGreen)
cell.SetAttributes(tcell.AttrBold)
v.SetCell(r, c, cell)
cell.SetExpansion(2)
cell.SetAlign(tview.AlignLeft)
v.SetCell(r, c+1, cell)
row++
for _, h := range hh {
col := c
cell := tview.NewTableCell(toMnemonic(h.Mnemonic))
cell.SetTextColor(tcell.ColorDodgerBlue)
if _, err := strconv.Atoi(h.Mnemonic); err != nil {
cell.SetTextColor(tcell.ColorDodgerBlue)
} else {
cell.SetTextColor(tcell.ColorFuchsia)
}
cell.SetAttributes(tcell.AttrBold)
cell.SetAlign(tview.AlignRight)
v.SetCell(row, col, cell)

View File

@ -42,7 +42,7 @@ func TestNewHelpView(t *testing.T) {
cfg := config.NewConfig(ks{})
a := NewApp(cfg)
v := newHelpView(a, nil, ui.Hints{{"blee", "duh"}})
v := newHelpView(a, nil, ui.Hints{{Mnemonic: "blee", Description: "duh"}})
v.Init(nil, "")
assert.Equal(t, "<blee>", v.GetCell(1, 0).Text)

View File

@ -12,8 +12,8 @@ type jobView struct {
*logResourceView
}
func newJobView(t string, app *appView, list resource.List) resourceViewer {
v := jobView{newLogResourceView(t, app, list)}
func newJobView(title, gvr string, app *appView, list resource.List) resourceViewer {
v := jobView{newLogResourceView(title, gvr, app, list)}
v.extraActionsFn = v.extraActions
v.enterFn = v.showPods

View File

@ -16,9 +16,9 @@ type (
}
)
func newLogResourceView(ns string, app *appView, list resource.List) *logResourceView {
func newLogResourceView(title, gvr string, app *appView, list resource.List) *logResourceView {
v := logResourceView{
resourceView: newResourceView(ns, app, list).(*resourceView),
resourceView: newResourceView(title, gvr, app, list).(*resourceView),
}
v.AddPage("logs", newLogsView(list.GetName(), app, &v), true, false)

View File

@ -10,8 +10,8 @@ type nodeView struct {
*resourceView
}
func newNodeView(t string, app *appView, list resource.List) resourceViewer {
v := nodeView{newResourceView(t, app, list).(*resourceView)}
func newNodeView(title, gvr string, app *appView, list resource.List) resourceViewer {
v := nodeView{newResourceView(title, gvr, app, list).(*resourceView)}
v.extraActionsFn = v.extraActions
v.enterFn = v.showPods
@ -19,10 +19,10 @@ func newNodeView(t string, app *appView, list resource.List) resourceViewer {
}
func (v *nodeView) extraActions(aa ui.KeyActions) {
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", v.sortColCmd(7, false), true)
aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", v.sortColCmd(8, false), true)
aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", v.sortColCmd(9, false), true)
aa[ui.KeyShiftZ] = ui.NewKeyAction("Sort MEM%", v.sortColCmd(10, false), true)
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", v.sortColCmd(7, false), false)
aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", v.sortColCmd(8, false), false)
aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", v.sortColCmd(9, false), false)
aa[ui.KeyShiftZ] = ui.NewKeyAction("Sort MEM%", v.sortColCmd(10, false), false)
}
func (v *nodeView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
@ -50,7 +50,7 @@ func showPods(app *appView, ns, labelSel, fieldSel string, a ui.ActionHandler) {
list.SetLabelSelector(labelSel)
list.SetFieldSelector(fieldSel)
pv := newPodView("Pods", app, list)
pv := newPodView("Pods", "v1/pods", app, list)
pv.setColorerFn(podColorer)
// pv.setExtraActionsFn(func(aa ui.KeyActions) {
pv.masterPage().SetActions(ui.KeyActions{

View File

@ -21,8 +21,8 @@ type namespaceView struct {
*resourceView
}
func newNamespaceView(t string, app *appView, list resource.List) resourceViewer {
v := namespaceView{newResourceView(t, app, list).(*resourceView)}
func newNamespaceView(title, gvr string, app *appView, list resource.List) resourceViewer {
v := namespaceView{newResourceView(title, gvr, app, list).(*resourceView)}
v.extraActionsFn = v.extraActions
v.masterPage().SetSelectedFn(v.cleanser)
v.decorateFn = v.decorate

View File

@ -32,8 +32,8 @@ type loggable interface {
switchPage(n string)
}
func newPodView(t string, app *appView, list resource.List) resourceViewer {
v := podView{resourceView: newResourceView(t, app, list).(*resourceView)}
func newPodView(title, gvr string, app *appView, list resource.List) resourceViewer {
v := podView{resourceView: newResourceView(title, gvr, app, list).(*resourceView)}
v.extraActionsFn = v.extraActions
v.enterFn = v.listContainers
@ -56,15 +56,15 @@ func (v *podView) extraActions(aa ui.KeyActions) {
aa[ui.KeyL] = ui.NewKeyAction("Logs", v.logsCmd, true)
aa[ui.KeyShiftL] = ui.NewKeyAction("Logs Previous", v.prevLogsCmd, true)
aa[ui.KeyShiftR] = ui.NewKeyAction("Sort Ready", v.sortColCmd(1, false), true)
aa[ui.KeyShiftS] = ui.NewKeyAction("Sort Status", v.sortColCmd(2, true), true)
aa[ui.KeyShiftT] = ui.NewKeyAction("Sort Restart", v.sortColCmd(3, false), true)
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", v.sortColCmd(4, false), true)
aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", v.sortColCmd(5, false), true)
aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", v.sortColCmd(6, false), true)
aa[ui.KeyShiftZ] = ui.NewKeyAction("Sort MEM%", v.sortColCmd(7, false), true)
aa[ui.KeyShiftD] = ui.NewKeyAction("Sort IP", v.sortColCmd(8, true), true)
aa[ui.KeyShiftO] = ui.NewKeyAction("Sort Node", v.sortColCmd(9, true), true)
aa[ui.KeyShiftR] = ui.NewKeyAction("Sort Ready", v.sortColCmd(1, false), false)
aa[ui.KeyShiftS] = ui.NewKeyAction("Sort Status", v.sortColCmd(2, true), false)
aa[ui.KeyShiftT] = ui.NewKeyAction("Sort Restart", v.sortColCmd(3, false), false)
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", v.sortColCmd(4, false), false)
aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", v.sortColCmd(5, false), false)
aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", v.sortColCmd(6, false), false)
aa[ui.KeyShiftZ] = ui.NewKeyAction("Sort MEM%", v.sortColCmd(7, false), false)
aa[ui.KeyShiftD] = ui.NewKeyAction("Sort IP", v.sortColCmd(8, true), false)
aa[ui.KeyShiftO] = ui.NewKeyAction("Sort Node", v.sortColCmd(9, true), false)
}
func (v *podView) listContainers(app *appView, _, res, sel string) {

View File

@ -76,10 +76,10 @@ func (v *policyView) bindKeys() {
tcell.KeyEscape: ui.NewKeyAction("Reset", v.resetCmd, false),
ui.KeySlash: ui.NewKeyAction("Filter", v.activateCmd, false),
ui.KeyP: ui.NewKeyAction("Previous", v.app.prevCmd, false),
ui.KeyShiftS: ui.NewKeyAction("Sort Namespace", v.SortColCmd(0), true),
ui.KeyShiftN: ui.NewKeyAction("Sort Name", v.SortColCmd(1), true),
ui.KeyShiftO: ui.NewKeyAction("Sort Group", v.SortColCmd(2), true),
ui.KeyShiftB: ui.NewKeyAction("Sort Binding", v.SortColCmd(3), true),
ui.KeyShiftS: ui.NewKeyAction("Sort Namespace", v.SortColCmd(0), false),
ui.KeyShiftN: ui.NewKeyAction("Sort Name", v.SortColCmd(1), false),
ui.KeyShiftO: ui.NewKeyAction("Sort Group", v.SortColCmd(2), false),
ui.KeyShiftB: ui.NewKeyAction("Sort Binding", v.SortColCmd(3), false),
})
}
@ -96,8 +96,8 @@ func (v *policyView) refresh() {
}
func (v *policyView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.Cmd().Empty() {
v.Cmd().Reset()
if !v.SearchBuff().Empty() {
v.SearchBuff().Reset()
return nil
}
@ -109,8 +109,8 @@ func (v *policyView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
v.cancel()
}
if v.Cmd().IsActive() {
v.Cmd().Reset()
if v.SearchBuff().IsActive() {
v.SearchBuff().Reset()
return nil
}

View File

@ -125,7 +125,7 @@ func (v *rbacView) bindKeys() {
tcell.KeyEscape: ui.NewKeyAction("Reset", v.resetCmd, false),
ui.KeySlash: ui.NewKeyAction("Filter", v.activateCmd, false),
ui.KeyP: ui.NewKeyAction("Previous", v.app.prevCmd, false),
ui.KeyShiftO: ui.NewKeyAction("Sort APIGroup", v.SortColCmd(1), true),
ui.KeyShiftO: ui.NewKeyAction("Sort APIGroup", v.SortColCmd(1), false),
})
}
@ -142,8 +142,8 @@ func (v *rbacView) refresh() {
}
func (v *rbacView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.Cmd().Empty() {
v.Cmd().Reset()
if !v.SearchBuff().Empty() {
v.SearchBuff().Reset()
return nil
}
@ -155,8 +155,8 @@ func (v *rbacView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
v.cancel()
}
if v.Cmd().IsActive() {
v.Cmd().Reset()
if v.SearchBuff().IsActive() {
v.SearchBuff().Reset()
return nil
}

View File

@ -1,10 +1,10 @@
package views
import (
"fmt"
"path"
"strings"
"time"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/resource"
"github.com/derailed/k9s/internal/ui"
@ -13,22 +13,14 @@ import (
)
type (
viewFn func(ns string, app *appView, list resource.List) resourceViewer
viewFn func(title, gvr string, app *appView, list resource.List) resourceViewer
listFn func(c resource.Connection, ns string) resource.List
enterFn func(app *appView, ns, resource, selection string)
decorateFn func(resource.TableData) resource.TableData
crdCmd struct {
api string
version string
plural string
singular string
}
resCmd struct {
crdCmd
viewer struct {
gvr string
kind string
namespaced bool
verbs metav1.Verbs
viewFn viewFn
@ -38,51 +30,25 @@ type (
decorateFn decorateFn
}
AliasConfig struct {
Aliases map[string]string `yaml:"aliases"`
}
viewers map[string]viewer
)
var DefaultAliasConfig = AliasConfig{
Aliases: map[string]string{
"Deployment": "dp",
"Secret": "sec",
"Jobs": "jo",
"ClusterRoles": "cr",
"ClusterRoleBindings": "crb",
"RoleBindings": "rb",
"Roles": "ro",
"NetworkPolicies": "np",
"Contexts": "ctx",
"Users": "usr",
"Groups": "grp",
"PortForward": "pf",
"Benchmark": "be",
"ScreenDumps": "sd",
},
}
func aliasCmds(c k8s.Connection, m map[string]resCmd) {
resourceViews(c, m)
func aliasCmds(c k8s.Connection, vv viewers) {
resourceViews(c, vv)
if c != nil {
allCRDs(c, m)
allCRDs(c, vv)
}
}
func listFunc(l resource.List) viewFn {
return func(ns string, app *appView, list resource.List) resourceViewer {
return newResourceView(
ns,
app,
l,
)
return func(title, gvr string, app *appView, list resource.List) resourceViewer {
return newResourceView(title, gvr, app, l)
}
}
func allCRDs(c k8s.Connection, m map[string]resCmd) {
var aliases = config.NewAliases()
func allCRDs(c k8s.Connection, vv viewers) {
crds, err := resource.NewCustomResourceDefinitionList(c, resource.AllNamespaces).
Resource().
List(resource.AllNamespaces)
@ -91,30 +57,31 @@ func allCRDs(c k8s.Connection, m map[string]resCmd) {
return
}
t := time.Now()
var meta resource.TypeMeta
for _, crd := range crds {
ff := crd.ExtFields()
crd.ExtFields(&meta)
gvr := path.Join(ff["group"].(string), ff["version"].(string), ff["kind"].(string))
var name string
if p, ok := ff["plural"].(string); ok {
aliases[p] = gvr
name = p
gvr := k8s.NewGVR(meta.Group, meta.Version, meta.Plural)
gvrs := gvr.String()
if meta.Plural != "" {
aliases.Define(meta.Plural, gvrs)
}
if s, ok := ff["singular"].(string); ok {
aliases[s] = gvr
name = s
if meta.Singular != "" {
aliases.Define(meta.Singular, gvrs)
}
if aa, ok := ff["aliases"].([]interface{}); ok {
for _, a := range aa {
aliases[a.(string)] = gvr
}
for _, a := range meta.ShortNames {
aliases.Define(a, gvrs)
}
m[gvr] = resCmd{
gvr: gvr,
viewFn: listFunc(resource.NewCustomList(c, "", ff["group"].(string), ff["version"].(string), name)),
vv[gvrs] = viewer{
gvr: gvrs,
kind: meta.Kind,
viewFn: listFunc(resource.NewCustomList(c, meta.Namespaced, "", gvrs)),
colorerFn: ui.DefaultColorer,
}
}
log.Debug().Msgf("Loading CRDS %v", time.Since(t))
}
func showRBAC(app *appView, ns, resource, selection string) {
@ -156,40 +123,44 @@ func showSAPolicy(app *appView, _, _, selection string) {
app.inject(newPolicyView(app, mapFuSubject("ServiceAccount"), n))
}
type Aliases map[string]string
var aliases Aliases
func load(c k8s.Connection, viewRes map[string]resCmd) {
// cc := map[string]resCmd{}
aliases = make(Aliases, len(viewRes))
func load(c k8s.Connection, vv viewers) {
if err := aliases.Load(); err != nil {
log.Error().Err(err).Msg("No custom aliases defined in config")
}
rr, _ := c.DialOrDie().Discovery().ServerPreferredResources()
for _, r := range rr {
log.Debug().Msgf("Group %#v", r.GroupVersion)
for _, res := range r.APIResources {
log.Debug().Msgf("\tRes %s -- %q:%q -- %+v", res.Name, res.Group, res.Version, res.ShortNames)
gvr := path.Join(r.GroupVersion, res.Name)
// Get singular, plural, shortname and to alias under gvr name
if cmd, ok := viewRes[gvr]; !ok {
log.Error().Msgf(fmt.Sprintf(">> Missed %s", gvr))
} else {
log.Debug().Msgf("Res %#v", res)
cmd.namespaced = res.Namespaced
cmd.verbs = res.Verbs
cmd.gvr = gvr
viewRes[gvr] = cmd
aliases[strings.ToLower(res.Kind)] = gvr
aliases[res.Name] = gvr
aliases[res.SingularName] = gvr
for _, s := range res.ShortNames {
aliases[s] = gvr
}
gvr := k8s.ToGVR(r.GroupVersion, res.Name)
cmd, ok := vv[gvr.String()]
if !ok {
// log.Debug().Msgf(fmt.Sprintf(">> No viewer defined for `%s`", gvr))
continue
}
cmd.namespaced = res.Namespaced
cmd.kind = res.Kind
cmd.verbs = res.Verbs
cmd.gvr = gvr.String()
vv[gvr.String()] = cmd
gvrStr := gvr.String()
aliases.Define(
strings.ToLower(res.Kind), gvrStr,
res.Name, gvrStr,
)
if len(res.SingularName) > 0 {
aliases.Define(res.SingularName, gvrStr)
}
for _, s := range res.ShortNames {
aliases.Define(s, gvrStr)
}
}
}
}
func resourceViews(c k8s.Connection, m map[string]resCmd) {
func resourceViews(c k8s.Connection, m viewers) {
defer func(t time.Time) {
log.Debug().Msgf("Loading Views Elapsed %v", time.Since(t))
}(time.Now())
coreRes(m)
miscRes(m)
appsRes(m)
@ -203,171 +174,170 @@ func resourceViews(c k8s.Connection, m map[string]resCmd) {
load(c, m)
}
func coreRes(m map[string]resCmd) {
m["v1/nodes"] = resCmd{
func coreRes(vv viewers) {
vv["v1/nodes"] = viewer{
viewFn: newNodeView,
listFn: resource.NewNodeList,
colorerFn: nsColorer,
}
m["v1/namespaces"] = resCmd{
vv["v1/namespaces"] = viewer{
viewFn: newNamespaceView,
listFn: resource.NewNamespaceList,
colorerFn: nsColorer,
}
m["v1/pods"] = resCmd{
vv["v1/pods"] = viewer{
viewFn: newPodView,
listFn: resource.NewPodList,
colorerFn: podColorer,
}
m["v1/serviceaccounts"] = resCmd{
viewFn: newResourceView,
vv["v1/serviceaccounts"] = viewer{
listFn: resource.NewServiceAccountList,
enterFn: showSAPolicy,
}
m["v1/services"] = resCmd{
vv["v1/services"] = viewer{
viewFn: newSvcView,
listFn: resource.NewServiceList,
}
m["v1/configmaps"] = resCmd{
vv["v1/configmaps"] = viewer{
listFn: resource.NewConfigMapList,
}
m["v1/persistentvolumes"] = resCmd{
vv["v1/persistentvolumes"] = viewer{
listFn: resource.NewPersistentVolumeList,
colorerFn: pvColorer,
}
m["v1/persistentvolumeclaims"] = resCmd{
vv["v1/persistentvolumeclaims"] = viewer{
listFn: resource.NewPersistentVolumeClaimList,
colorerFn: pvcColorer,
}
m["v1/secrets"] = resCmd{
vv["v1/secrets"] = viewer{
viewFn: newSecretView,
listFn: resource.NewSecretList,
}
m["v1/endpoints"] = resCmd{
vv["v1/endpoints"] = viewer{
listFn: resource.NewEndpointsList,
}
m["v1/events"] = resCmd{
vv["v1/events"] = viewer{
listFn: resource.NewEventList,
colorerFn: evColorer,
}
m["v1/replicationcontrollers"] = resCmd{
vv["v1/replicationcontrollers"] = viewer{
viewFn: newScalableResourceView,
listFn: resource.NewReplicationControllerList,
colorerFn: rsColorer,
}
}
func miscRes(m map[string]resCmd) {
m["storage.k8s.io/storageclasses"] = resCmd{
func miscRes(vv viewers) {
vv["storage.k8s.io/v1/storageclasses"] = viewer{
listFn: resource.NewStorageClassList,
}
m["ctx"] = resCmd{
gvr: "Contexts",
vv["contexts"] = viewer{
gvr: "contexts",
kind: "Contexts",
viewFn: newContextView,
listFn: resource.NewContextList,
colorerFn: ctxColorer,
}
m["usr"] = resCmd{
vv["users"] = viewer{
gvr: "users",
viewFn: newSubjectView,
}
m["grp"] = resCmd{
vv["groups"] = viewer{
gvr: "groups",
viewFn: newSubjectView,
}
m["pf"] = resCmd{
gvr: "PortForward",
vv["portforwards"] = viewer{
gvr: "portforwards",
viewFn: newForwardView,
}
m["be"] = resCmd{
gvr: "Benchmark",
vv["benchmarks"] = viewer{
gvr: "benchmarks",
viewFn: newBenchView,
}
m["sd"] = resCmd{
gvr: "ScreenDumps",
vv["screendumps"] = viewer{
gvr: "screendumps",
viewFn: newDumpView,
}
}
func appsRes(m map[string]resCmd) {
m["apps/v1/deployments"] = resCmd{
func appsRes(vv viewers) {
vv["apps/v1/deployments"] = viewer{
viewFn: newDeployView,
listFn: resource.NewDeploymentList,
colorerFn: dpColorer,
}
m["apps/v1/replicasets"] = resCmd{
vv["apps/v1/replicasets"] = viewer{
viewFn: newReplicaSetView,
listFn: resource.NewReplicaSetList,
colorerFn: rsColorer,
}
m["apps/v1/statefulsets"] = resCmd{
vv["apps/v1/statefulsets"] = viewer{
viewFn: newStatefulSetView,
listFn: resource.NewStatefulSetList,
colorerFn: stsColorer,
}
m["apps/v1/daemonsets"] = resCmd{
vv["apps/v1/daemonsets"] = viewer{
viewFn: newDaemonSetView,
listFn: resource.NewDaemonSetList,
colorerFn: dpColorer,
}
}
func authRes(m map[string]resCmd) {
m["rbac.authorization.k8s.io/v1/clusterroles"] = resCmd{
func authRes(vv viewers) {
vv["rbac.authorization.k8s.io/v1/clusterroles"] = viewer{
listFn: resource.NewClusterRoleList,
enterFn: showRBAC,
}
m["rbac.authorization.k8s.io/v1/clusterrolebindings"] = resCmd{
vv["rbac.authorization.k8s.io/v1/clusterrolebindings"] = viewer{
listFn: resource.NewClusterRoleBindingList,
enterFn: showClusterRole,
}
m["rbac.authorization.k8s.io/v1/rolebindings"] = resCmd{
vv["rbac.authorization.k8s.io/v1/rolebindings"] = viewer{
listFn: resource.NewRoleBindingList,
enterFn: showRole,
}
m["rbac.authorization.k8s.io/v1/roles"] = resCmd{
vv["rbac.authorization.k8s.io/v1/roles"] = viewer{
listFn: resource.NewRoleList,
enterFn: showRBAC,
}
}
func extRes(m map[string]resCmd) {
m["apiextensions.k8s.io/v1/customresourcedefinitions"] = resCmd{
func extRes(vv viewers) {
vv["apiextensions.k8s.io/v1/customresourcedefinitions"] = viewer{
listFn: resource.NewCustomResourceDefinitionList,
enterFn: showCRD,
}
}
func netRes(m map[string]resCmd) {
m["networking.k8s.io/v1/networkpolicies"] = resCmd{
gvr: "apiextensions.k8s.io/NetworkPolicies",
func netRes(vv viewers) {
vv["networking.k8s.io/v1/networkpolicies"] = viewer{
listFn: resource.NewNetworkPolicyList,
}
m["networking.k8s.io/v1beta1/ingresses"] = resCmd{
vv["extensions/v1beta1/ingresses"] = viewer{
listFn: resource.NewIngressList,
}
}
func batchRes(m map[string]resCmd) {
m["batch/v1/cronjobs"] = resCmd{
func batchRes(vv viewers) {
vv["batch/v1beta1/cronjobs"] = viewer{
viewFn: newCronJobView,
listFn: resource.NewCronJobList,
}
m["batch/v1/jobs"] = resCmd{
vv["batch/v1/jobs"] = viewer{
viewFn: newJobView,
listFn: resource.NewJobList,
}
}
func policyRes(m map[string]resCmd) {
m["policy/v1beta1/poddisruptionbudgets"] = resCmd{
viewFn: newResourceView,
func policyRes(vv viewers) {
vv["policy/v1beta1/poddisruptionbudgets"] = viewer{
listFn: resource.NewPDBList,
colorerFn: pdbColorer,
}
}
func hpaRes(m map[string]resCmd) {
m["autoscaling/v1/horizontalpodautoscalers"] = resCmd{
func hpaRes(vv viewers) {
vv["autoscaling/v1/horizontalpodautoscalers"] = viewer{
listFn: resource.NewHorizontalPodAutoscalerV1List,
}
}

View File

@ -37,12 +37,14 @@ type (
colorerFn ui.ColorerFunc
decorateFn decorateFn
envFn envFn
gvr string
}
)
func newResourceView(title string, app *appView, list resource.List) resourceViewer {
func newResourceView(title, gvr string, app *appView, list resource.List) resourceViewer {
v := resourceView{
list: list,
gvr: gvr,
}
v.masterDetail = newMasterDetail(title, list.GetNamespace(), app, v.backCmd)
v.envFn = v.defaultK9sEnv
@ -223,7 +225,7 @@ func (v *resourceView) defaultEnter(ns, _, selection string) {
return
}
yaml, err := v.list.Resource().Describe(v.masterPage().GetBaseTitle(), selection)
yaml, err := v.list.Resource().Describe(v.gvr, selection)
if err != nil {
v.app.Flash().Errf("Describe command failed: %s", err)
return
@ -316,7 +318,7 @@ func (v *resourceView) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
v.refresh()
v.masterPage().UpdateTitle()
v.masterPage().SelectRow(1, true)
v.app.GetCmdBuff().Reset()
v.app.CmdBuff().Reset()
v.app.Config.SetActiveNamespace(v.currentNS)
v.app.Config.Save()

View File

@ -23,8 +23,8 @@ type replicaSetView struct {
*resourceView
}
func newReplicaSetView(t string, app *appView, list resource.List) resourceViewer {
v := replicaSetView{newResourceView(t, app, list).(*resourceView)}
func newReplicaSetView(title, gvr string, app *appView, list resource.List) resourceViewer {
v := replicaSetView{newResourceView(title, gvr, app, list).(*resourceView)}
v.extraActionsFn = v.extraActions
v.enterFn = v.showPods
@ -32,8 +32,8 @@ func newReplicaSetView(t string, app *appView, list resource.List) resourceViewe
}
func (v *replicaSetView) extraActions(aa ui.KeyActions) {
aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(2, false), true)
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(3, false), true)
aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(2, false), false)
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(3, false), false)
aa[tcell.KeyCtrlB] = ui.NewKeyAction("Rollback", v.rollbackCmd, true)
}

View File

@ -17,8 +17,8 @@ type (
}
)
func newScalableResourceView(title string, app *appView, list resource.List) resourceViewer {
return *newScalableResourceViewForParent(newResourceView(title, app, list).(*resourceView))
func newScalableResourceView(title, gvr string, app *appView, list resource.List) resourceViewer {
return *newScalableResourceViewForParent(newResourceView(title, gvr, app, list).(*resourceView))
}
func newScalableResourceViewForParent(parent *resourceView) *scalableResourceView {

View File

@ -13,8 +13,8 @@ type secretView struct {
*resourceView
}
func newSecretView(t string, app *appView, list resource.List) resourceViewer {
v := secretView{newResourceView(t, app, list).(*resourceView)}
func newSecretView(title, gvr string, app *appView, list resource.List) resourceViewer {
v := secretView{newResourceView(title, gvr, app, list).(*resourceView)}
v.extraActionsFn = v.extraActions
return &v

View File

@ -14,8 +14,8 @@ type statefulSetView struct {
scalableResourceView *scalableResourceView
}
func newStatefulSetView(t string, app *appView, list resource.List) resourceViewer {
logResourceView := newLogResourceView(t, app, list)
func newStatefulSetView(title, gvr string, app *appView, list resource.List) resourceViewer {
logResourceView := newLogResourceView(title, gvr, app, list)
v := statefulSetView{logResourceView, newScalableResourceViewForParent(logResourceView.resourceView)}
v.extraActionsFn = v.extraActions
v.enterFn = v.showPods
@ -26,8 +26,8 @@ func newStatefulSetView(t string, app *appView, list resource.List) resourceView
func (v *statefulSetView) extraActions(aa ui.KeyActions) {
v.logResourceView.extraActions(aa)
v.scalableResourceView.extraActions(aa)
aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(1, false), true)
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(2, false), true)
aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", v.sortColCmd(1, false), false)
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", v.sortColCmd(2, false), false)
}
func (v *statefulSetView) showPods(app *appView, ns, res, sel string) {

View File

@ -33,7 +33,7 @@ type (
}
)
func newSubjectView(ns string, app *appView, list resource.List) resourceViewer {
func newSubjectView(title, gvr string, app *appView, list resource.List) resourceViewer {
v := subjectView{}
v.tableView = newTableView(app, "Subject")
v.SetActiveNS("*")
@ -95,7 +95,7 @@ func (v *subjectView) bindKeys() {
tcell.KeyEscape: ui.NewKeyAction("Reset", v.resetCmd, false),
ui.KeySlash: ui.NewKeyAction("Filter", v.activateCmd, false),
ui.KeyP: ui.NewKeyAction("Previous", v.app.prevCmd, false),
ui.KeyShiftK: ui.NewKeyAction("Sort Kind", v.SortColCmd(1), true),
ui.KeyShiftK: ui.NewKeyAction("Sort Kind", v.SortColCmd(1), false),
})
}
@ -136,8 +136,8 @@ func (v *subjectView) policyCmd(evt *tcell.EventKey) *tcell.EventKey {
}
func (v *subjectView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.Cmd().Empty() {
v.Cmd().Reset()
if !v.SearchBuff().Empty() {
v.SearchBuff().Reset()
return nil
}
@ -149,8 +149,8 @@ func (v *subjectView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
v.cancel()
}
if v.Cmd().IsActive() {
v.Cmd().Reset()
if v.SearchBuff().IsActive() {
v.SearchBuff().Reset()
return nil
}
@ -285,8 +285,9 @@ func (v *subjectView) namespacedSubjects() (resource.RowEvents, error) {
}
func mapCmdSubject(subject string) string {
log.Debug().Msgf("!!!!!!Subject %q", subject)
switch subject {
case "grp":
case "groups":
return "Group"
case "sas":
return "ServiceAccount"

View File

@ -22,9 +22,9 @@ type svcView struct {
bench *perf.Benchmark
}
func newSvcView(t string, app *appView, list resource.List) resourceViewer {
func newSvcView(title, gvr string, app *appView, list resource.List) resourceViewer {
v := svcView{
resourceView: newResourceView(t, app, list).(*resourceView),
resourceView: newResourceView(title, gvr, app, list).(*resourceView),
}
v.extraActionsFn = v.extraActions
v.enterFn = v.showPods
@ -47,7 +47,7 @@ func (v *svcView) extraActions(aa ui.KeyActions) {
aa[ui.KeyL] = ui.NewKeyAction("Logs", v.logsCmd, true)
aa[tcell.KeyCtrlB] = ui.NewKeyAction("Bench", v.benchCmd, true)
aa[tcell.KeyCtrlK] = ui.NewKeyAction("Bench Stop", v.benchStopCmd, true)
aa[ui.KeyShiftT] = ui.NewKeyAction("Sort Type", v.sortColCmd(1, false), true)
aa[ui.KeyShiftT] = ui.NewKeyAction("Sort Type", v.sortColCmd(1, false), false)
}
func (v *svcView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
@ -217,7 +217,7 @@ func (v *svcView) showSvcPods(ns string, sel map[string]string, b ui.ActionHandl
list := resource.NewPodList(v.app.Conn(), ns)
list.SetLabelSelector(strings.Join(s, ","))
pv := newPodView("Pods", v.app, list)
pv := newPodView("Pods", "v1/pods", v.app, list)
pv.setColorerFn(podColorer)
pv.setExtraActionsFn(func(aa ui.KeyActions) {
aa[tcell.KeyEsc] = ui.NewKeyAction("Back", b, true)

View File

@ -17,14 +17,23 @@ func newTableView(app *appView, title string) *tableView {
Table: ui.NewTable(title, app.Styles),
app: app,
}
v.Cmd().AddListener(app.Cmd())
v.Cmd().Reset()
v.SearchBuff().AddListener(app.Cmd())
v.SearchBuff().AddListener(&v)
v.SearchBuff().Reset()
v.bindKeys()
return &v
}
// BufferChanged indicates the buffer was changed.
func (v *tableView) BufferChanged(s string) {}
// BufferActive indicates the buff activity changed.
func (v *tableView) BufferActive(state bool) {
v.app.BufferActive(state)
}
func (v *tableView) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
if path, err := saveTable(v.app.Config.K9s.CurrentCluster, v.GetBaseTitle(), v.GetData()); err != nil {
v.app.Flash().Err(err)
@ -49,18 +58,18 @@ func (v *tableView) bindKeys() {
tcell.KeyBackspace: ui.NewKeyAction("Erase", v.eraseCmd, false),
tcell.KeyDelete: ui.NewKeyAction("Erase", v.eraseCmd, false),
ui.KeyShiftI: ui.NewKeyAction("Invert", v.SortInvertCmd, false),
ui.KeyShiftN: ui.NewKeyAction("Sort Name", v.SortColCmd(0), true),
ui.KeyShiftA: ui.NewKeyAction("Sort Age", v.SortColCmd(-1), true),
ui.KeyShiftN: ui.NewKeyAction("Sort Name", v.SortColCmd(0), false),
ui.KeyShiftA: ui.NewKeyAction("Sort Age", v.SortColCmd(-1), false),
})
}
func (v *tableView) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.Cmd().IsActive() {
if !v.SearchBuff().IsActive() {
return evt
}
v.Cmd().SetActive(false)
cmd := v.Cmd().String()
v.SearchBuff().SetActive(false)
cmd := v.SearchBuff().String()
if isLabelSelector(cmd) && v.filterFn != nil {
v.filterFn(trimLabelSelector(cmd))
return nil
@ -71,21 +80,21 @@ func (v *tableView) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
}
func (v *tableView) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.Cmd().IsActive() {
v.Cmd().Delete()
if v.SearchBuff().IsActive() {
v.SearchBuff().Delete()
}
return nil
}
func (v *tableView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.Cmd().Empty() {
if !v.SearchBuff().Empty() {
v.app.Flash().Info("Clearing filter...")
}
if isLabelSelector(v.Cmd().String()) {
if isLabelSelector(v.SearchBuff().String()) {
v.filterFn("")
}
v.Cmd().Reset()
v.SearchBuff().Reset()
v.Refresh()
return nil
@ -97,11 +106,11 @@ func (v *tableView) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
}
v.app.Flash().Info("Filter mode activated.")
if isLabelSelector(v.Cmd().String()) {
if isLabelSelector(v.SearchBuff().String()) {
return nil
}
v.Cmd().Reset()
v.Cmd().SetActive(true)
v.SearchBuff().Reset()
v.SearchBuff().SetActive(true)
return nil
}

View File

@ -72,8 +72,8 @@ func TestTableViewFilter(t *testing.T) {
Namespace: "",
}
v.Update(data)
v.Cmd().SetActive(true)
v.Cmd().Set([]rune("blee"))
v.SearchBuff().SetActive(true)
v.SearchBuff().Set([]rune("blee"))
v.filterCmd(nil)
assert.Equal(t, 2, v.GetRowCount())
v.resetCmd(nil)