fix context/namespace support + bug fixes + added crumbs
parent
57c77ca557
commit
0721e66a05
104
README.md
104
README.md
|
|
@ -80,10 +80,17 @@ k9s --context coolCtx
|
|||
|
||||
```yaml
|
||||
k9s:
|
||||
# Indicates api-server poll intervals.
|
||||
refreshRate: 2
|
||||
# Indicates log view maximum buffer size. Default 1k lines.
|
||||
logBufferSize: 200
|
||||
# Indicates how many lines of logs to retrieve from the api-server. Default 200 lines.
|
||||
logRequestSize: 200
|
||||
# Indicates the current kube context. Defaults to current context
|
||||
currentContext: minikube
|
||||
# Indicates the current kube cluster. Defaults to current context cluster
|
||||
currentCluster: minikube
|
||||
# Persists per cluster preferences for favorite namespaces and view.
|
||||
clusters:
|
||||
bitchn:
|
||||
namespace:
|
||||
|
|
@ -143,12 +150,103 @@ K9s uses aliases to navigate most K8s resources.
|
|||
|
||||
This initial drop is brittle. K9s will most likely blow up...
|
||||
|
||||
1. You're running older versions of Kubernetes. K9s works best Kubernetes 1.10+
|
||||
1. You don't have enough RBAC fu to manage your cluster
|
||||
1. You're running older versions of Kubernetes. K9s works best Kubernetes 1.10+.
|
||||
1. You don't have enough RBAC fu to manage your cluster (see RBAC section below).
|
||||
1. Your cluster does not run a metric server.
|
||||
|
||||
---
|
||||
|
||||
## K9s RBAC FU
|
||||
|
||||
On RBAC enabled clusters, you would need to give your users/groups capabilities so that they can use K9s to explore Kubernetes cluster.
|
||||
K9s needs minimaly read privileges at both the cluster and namespace level to display resources and metrics.
|
||||
|
||||
These rules below are just suggestions. You will need to customize them based on your environment policies. If you need to edit/delete resources extra Fu will be necessary.
|
||||
|
||||
> NOTE! Cluster/Namespace access may change in the future as K9s evolves.
|
||||
|
||||
> NOTE! We expect K9s to keep running even in atrophied clusters/namespaces. Please file issues if this is not the case!
|
||||
|
||||
### Cluster RBAC scope
|
||||
|
||||
```yaml
|
||||
---
|
||||
# K9s Reader ClusterRole
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: k9s
|
||||
rules:
|
||||
# Grants RO access to cluster resources node and namespace
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes", "namespaces"]
|
||||
verbs: ["get", "list"]
|
||||
# Grants RO access to RBAC resources
|
||||
- apiGroups: ["rbac.authorization.k8s.io"]
|
||||
resources: ["clusterroles", "roles", "clusterrolebindings", "rolebindings"]
|
||||
verbs: ["get", "list"]
|
||||
# Grants RO access to CRD resources
|
||||
- apiGroups: ["apiextensions.k8s.io"]
|
||||
resources: ["customresourcedefinitions"]
|
||||
verbs: ["get", "list"]
|
||||
# Grants RO access to netric server
|
||||
- apiGroups: ["metrics.k8s.io"]
|
||||
resources: ["nodes", "pods"]
|
||||
verbs: ["list"]
|
||||
|
||||
---
|
||||
# Sample K9s user ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: k9s
|
||||
subjects:
|
||||
- kind: User
|
||||
name: fernand
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: k9s
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
### Namespace RBAC scope
|
||||
|
||||
If your users are constrained to certain namespaces, K9s will need to following role to enable read access to namespaced resources.
|
||||
|
||||
```yaml
|
||||
---
|
||||
# K9s Reader Role (default namespace)
|
||||
kind: Role
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: k9s
|
||||
namespace: default
|
||||
rules:
|
||||
# Grants RO access to most namespaced resources
|
||||
- apiGroups: ["", "apps", "autoscaling", "batch", "extensions"]
|
||||
resources: ["*"]
|
||||
verbs: ["get", "list"]
|
||||
|
||||
---
|
||||
# Sample K9s user RoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: k9s
|
||||
namespace: default
|
||||
subjects:
|
||||
- kind: User
|
||||
name: fernand
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: k9s
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This is still work in progress! If there is enough interest in the Kubernetes
|
||||
|
|
@ -169,7 +267,7 @@ to make this project a reality!
|
|||
## Contact Info
|
||||
|
||||
1. **Email**: fernand@imhotep.io
|
||||
1. **Twitter**: [@kitesurfer](https://twitter.com/kitesurfer?lang=en)
|
||||
2. **Twitter**: [@kitesurfer](https://twitter.com/kitesurfer?lang=en)
|
||||
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
# Release v0.2.5
|
||||
|
||||
## Notes
|
||||
|
||||
Thank you to all that contributed with flushing out issues with K9s! I'll try
|
||||
to mark some of these issues as fixed. But if you don't mind grab the latest
|
||||
rev and see if we're happier with some of the fixes!
|
||||
|
||||
If you've filed an issue please help me verify and close.
|
||||
|
||||
Thank you so much for your support!!
|
||||
|
||||
---
|
||||
|
||||
## Change Logs
|
||||
|
||||
+ Added an actual help view to show available key bindings. Use `<?>` to access it.
|
||||
+ Changed alias view to now be accessible via key `<a>`
|
||||
+ Pressing `<enter>` while on the namespace/context views will navigate directly to the pods view.
|
||||
+ Added resource view breadcrumbs to easily navigate back in history. Use key `<p>` to navigate back.
|
||||
+ Added configuration `logBufferSize` to limit the size of the log view while viewing chatty or big logs.
|
||||
|
||||
---
|
||||
|
||||
## Resolved Bugs
|
||||
|
||||
+ [Issue #116](https://github.com/derailed/k9s/issues/116)
|
||||
+ [Issue #113](https://github.com/derailed/k9s/issues/113)
|
||||
+ [Issue #111](https://github.com/derailed/k9s/issues/111)
|
||||
+ [Issue #110](https://github.com/derailed/k9s/issues/110)
|
||||
19
cmd/info.go
19
cmd/info.go
|
|
@ -2,9 +2,9 @@ package cmd
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/printer"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -14,18 +14,11 @@ func infoCmd() *cobra.Command {
|
|||
Short: "Print configuration information",
|
||||
Long: "Print configuration information",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
const (
|
||||
cyan = "\033[1;36m%s\033[0m"
|
||||
green = "\033[1;32m%s\033[0m"
|
||||
magenta = "\033[1;35m%s\033[0m"
|
||||
)
|
||||
fmt.Printf(cyan+"\n", strings.Repeat("-", 80))
|
||||
fmt.Printf(green+"\n", "🐶 K9s Information")
|
||||
fmt.Printf(magenta, fmt.Sprintf("%-10s", "LogFile:"))
|
||||
fmt.Printf("%s\n", config.K9sLogs)
|
||||
fmt.Printf(magenta, fmt.Sprintf("%-10s", "Config:"))
|
||||
fmt.Printf("%s\n", config.K9sConfigFile)
|
||||
fmt.Printf(cyan+"\n", strings.Repeat("-", 80))
|
||||
fmt.Printf(printer.Colorize(fmt.Sprintf("%-15s", "Configuration:"), printer.ColorMagenta))
|
||||
fmt.Println(printer.Colorize(config.K9sConfigFile, printer.ColorDarkGray))
|
||||
|
||||
fmt.Printf(printer.Colorize(fmt.Sprintf("%-15s", "Logs:"), printer.ColorMagenta))
|
||||
fmt.Println(printer.Colorize(config.K9sLogs, printer.ColorDarkGray))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
176
cmd/root.go
176
cmd/root.go
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/printer"
|
||||
"github.com/derailed/k9s/internal/views"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
|
@ -14,22 +15,23 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
appName = "k9s"
|
||||
defaultRefreshRate = 2 // secs
|
||||
defaultLogLevel = "info"
|
||||
shortAppDesc = "A graphical CLI for your Kubernetes cluster management."
|
||||
longAppDesc = "K9s is a CLI to view and manage your Kubernetes clusters."
|
||||
)
|
||||
|
||||
var (
|
||||
version = "dev"
|
||||
commit = "dev"
|
||||
date = "n/a"
|
||||
refreshRate int
|
||||
logLevel string
|
||||
k8sFlags *genericclioptions.ConfigFlags
|
||||
version, commit, date = "dev", "dev", "n/a"
|
||||
refreshRate int
|
||||
logLevel string
|
||||
k8sFlags *genericclioptions.ConfigFlags
|
||||
|
||||
rootCmd = &cobra.Command{
|
||||
Use: "k9s",
|
||||
Short: "A graphical CLI for your Kubernetes cluster management.",
|
||||
Long: `K9s is a CLI to view and manage your Kubernetes clusters.`,
|
||||
Use: appName,
|
||||
Short: shortAppDesc,
|
||||
Long: longAppDesc,
|
||||
Run: run,
|
||||
}
|
||||
_ config.KubeSettings = &k8s.Config{}
|
||||
|
|
@ -37,63 +39,77 @@ var (
|
|||
|
||||
func init() {
|
||||
rootCmd.AddCommand(versionCmd(), infoCmd())
|
||||
|
||||
rootCmd.Flags().IntVarP(
|
||||
&refreshRate,
|
||||
"refresh", "r",
|
||||
defaultRefreshRate,
|
||||
"Specifies the default refresh rate as an integer (sec)",
|
||||
)
|
||||
rootCmd.Flags().StringVarP(
|
||||
&logLevel,
|
||||
"logLevel", "l",
|
||||
defaultLogLevel,
|
||||
"Specify a log level (info, warn, debug, error, fatal, panic, trace)",
|
||||
)
|
||||
|
||||
initK9sFlags()
|
||||
initK8sFlags()
|
||||
}
|
||||
|
||||
func initK9s() {
|
||||
// Execute root command
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Panic().Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run(cmd *cobra.Command, args []string) {
|
||||
defer func() {
|
||||
clearScreen()
|
||||
if err := recover(); err != nil {
|
||||
log.Error().Msgf("%v", err)
|
||||
log.Error().Msg(string(debug.Stack()))
|
||||
fmt.Printf(printer.Colorize("Boom!! ", printer.ColorRed))
|
||||
fmt.Println(printer.Colorize(fmt.Sprintf("%v.", err), printer.ColorDarkGray))
|
||||
// debug.PrintStack()
|
||||
}
|
||||
}()
|
||||
|
||||
zerolog.SetGlobalLevel(parseLevel(logLevel))
|
||||
loadConfiguration()
|
||||
app := views.NewApp()
|
||||
{
|
||||
defer app.Stop()
|
||||
app.Init(version, refreshRate, k8sFlags)
|
||||
app.Run()
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfiguration() {
|
||||
log.Info().Msg("🐶 K9s starting up...")
|
||||
|
||||
// Load K9s config file...
|
||||
cfg := k8s.NewConfig(k8sFlags)
|
||||
config.Root = config.NewConfig(cfg)
|
||||
initK9sConfig()
|
||||
|
||||
// Init K8s connection...
|
||||
k8s.InitConnectionOrDie(cfg)
|
||||
log.Info().Msg("✅ Kubernetes connectivity")
|
||||
|
||||
config.Root.Save()
|
||||
}
|
||||
|
||||
func initK9sConfig() {
|
||||
if err := config.Root.Load(config.K9sConfigFile); err != nil {
|
||||
log.Warn().Msg("Unable to locate K9s config. Generating new configuration...")
|
||||
}
|
||||
config.Root.K9s.RefreshRate = refreshRate
|
||||
mergeConfigs()
|
||||
// Init K8s connection...
|
||||
k8s.InitConnectionOrDie(cfg)
|
||||
log.Info().Msg("✅ Kubernetes connectivity")
|
||||
config.Root.Save()
|
||||
}
|
||||
|
||||
func mergeConfigs() {
|
||||
cfg, err := k8sFlags.ToRawKubeConfigLoader().RawConfig()
|
||||
if err != nil {
|
||||
panic("Invalid configuration. Unable to connect to api")
|
||||
}
|
||||
|
||||
ctx := cfg.CurrentContext
|
||||
switch {
|
||||
case isSet(k8sFlags.Context):
|
||||
ctx = *k8sFlags.Context
|
||||
config.Root.K9s.CurrentContext = ctx
|
||||
case isSet(&config.Root.K9s.CurrentContext):
|
||||
k8sFlags.Context = &config.Root.K9s.CurrentContext
|
||||
default:
|
||||
config.Root.K9s.CurrentContext = ctx
|
||||
if isSet(&cfg.Contexts[ctx].Namespace) {
|
||||
config.Root.SetActiveNamespace(cfg.Contexts[ctx].Namespace)
|
||||
}
|
||||
if isSet(k8sFlags.Context) {
|
||||
config.Root.K9s.CurrentContext = *k8sFlags.Context
|
||||
} else {
|
||||
config.Root.K9s.CurrentContext = cfg.CurrentContext
|
||||
}
|
||||
log.Debug().Msgf("Active Context `%v`", config.Root.K9s.CurrentContext)
|
||||
|
||||
if c, ok := cfg.Contexts[config.Root.K9s.CurrentContext]; ok {
|
||||
config.Root.K9s.CurrentCluster = c.Cluster
|
||||
if len(c.Namespace) != 0 {
|
||||
config.Root.SetActiveNamespace(c.Namespace)
|
||||
}
|
||||
} else {
|
||||
log.Panic().Msg(fmt.Sprintf("The specified context `%s does not exists in kubeconfig", config.Root.K9s.CurrentContext))
|
||||
}
|
||||
log.Debug().Msgf("Active Context `%v`", ctx)
|
||||
|
||||
if isSet(k8sFlags.Namespace) {
|
||||
config.Root.SetActiveNamespace(*k8sFlags.Namespace)
|
||||
|
|
@ -102,26 +118,6 @@ func initK9sConfig() {
|
|||
if isSet(k8sFlags.ClusterName) {
|
||||
config.Root.K9s.CurrentCluster = *k8sFlags.ClusterName
|
||||
}
|
||||
|
||||
if c, ok := cfg.Contexts[ctx]; ok {
|
||||
config.Root.K9s.CurrentCluster = c.Cluster
|
||||
if len(c.Namespace) != 0 {
|
||||
config.Root.SetActiveNamespace(c.Namespace)
|
||||
}
|
||||
} else {
|
||||
panic(fmt.Sprintf("The specified context `%s does not exists in kubeconfig", cfg.CurrentContext))
|
||||
}
|
||||
}
|
||||
|
||||
func isSet(s *string) bool {
|
||||
return s != nil && len(*s) > 0
|
||||
}
|
||||
|
||||
// Execute root command
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Panic().Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
func parseLevel(level string) zerolog.Level {
|
||||
|
|
@ -139,28 +135,19 @@ func parseLevel(level string) zerolog.Level {
|
|||
}
|
||||
}
|
||||
|
||||
func run(cmd *cobra.Command, args []string) {
|
||||
zerolog.SetGlobalLevel(parseLevel(logLevel))
|
||||
|
||||
initK9s()
|
||||
|
||||
app := views.NewApp()
|
||||
{
|
||||
app.Init(version, refreshRate, k8sFlags)
|
||||
defer func() {
|
||||
clearScreen()
|
||||
if err := recover(); err != nil {
|
||||
app.Stop()
|
||||
log.Error().Msgf("Boom! %#v", err)
|
||||
debug.PrintStack()
|
||||
}
|
||||
}()
|
||||
app.Run()
|
||||
}
|
||||
}
|
||||
|
||||
func clearScreen() {
|
||||
fmt.Print("\033[H\033[2J")
|
||||
func initK9sFlags() {
|
||||
rootCmd.Flags().IntVarP(
|
||||
&refreshRate,
|
||||
"refresh", "r",
|
||||
defaultRefreshRate,
|
||||
"Specifies the default refresh rate as an integer (sec)",
|
||||
)
|
||||
rootCmd.Flags().StringVarP(
|
||||
&logLevel,
|
||||
"logLevel", "l",
|
||||
defaultLogLevel,
|
||||
"Specify a log level (info, warn, debug, error, fatal, panic, trace)",
|
||||
)
|
||||
}
|
||||
|
||||
func initK8sFlags() {
|
||||
|
|
@ -257,3 +244,14 @@ func initK8sFlags() {
|
|||
"If present, the namespace scope for this CLI request",
|
||||
)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func clearScreen() {
|
||||
fmt.Print("\033[H\033[2J")
|
||||
}
|
||||
|
||||
func isSet(s *string) bool {
|
||||
return s != nil && len(*s) > 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package cmd
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/derailed/k9s/internal/printer"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -12,7 +13,13 @@ func versionCmd() *cobra.Command {
|
|||
Short: "Print version info",
|
||||
Long: "Prints version info",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("Version:%s GitCommit:%s On %s\n", version, commit, date)
|
||||
const secFmt = "%-10s"
|
||||
fmt.Printf(printer.Colorize(fmt.Sprintf(secFmt, "Version:"), printer.ColorMagenta))
|
||||
fmt.Println(printer.Colorize(version, printer.ColorDarkGray))
|
||||
fmt.Printf(printer.Colorize(fmt.Sprintf(secFmt, "Commit:"), printer.ColorMagenta))
|
||||
fmt.Println(printer.Colorize(commit, printer.ColorDarkGray))
|
||||
fmt.Printf(printer.Colorize(fmt.Sprintf(secFmt, "Date:"), printer.ColorMagenta))
|
||||
fmt.Println(printer.Colorize(date, printer.ColorDarkGray))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
12
go.mod
12
go.mod
|
|
@ -1,30 +1,37 @@
|
|||
module github.com/derailed/k9s
|
||||
|
||||
replace github.com/derailed/tview => /Users/fernand/go_wk/derailed/src/github.com/derailed/tview
|
||||
|
||||
require (
|
||||
contrib.go.opencensus.io/exporter/ocagent v0.4.3 // indirect
|
||||
github.com/Azure/go-autorest v11.4.0+incompatible // indirect
|
||||
github.com/derailed/tview v0.1.3
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/evanphx/json-patch v4.1.0+incompatible // indirect
|
||||
github.com/fatih/camelcase v1.0.0 // indirect
|
||||
github.com/gdamore/tcell v1.1.1
|
||||
github.com/gogo/protobuf v1.1.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
|
||||
github.com/googleapis/gnostic v0.2.0 // indirect
|
||||
github.com/gophercloud/gophercloud v0.0.0-20190206201033-83b528acebb4 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.1 // indirect
|
||||
github.com/imdario/mergo v0.3.6 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.5 // indirect
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.4
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/onsi/gomega v1.4.3 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
|
||||
github.com/pborman/uuid v1.2.0 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/petergtz/pegomock v0.0.0-20181206220228-b113d17a7e81
|
||||
github.com/rs/zerolog v1.12.0
|
||||
github.com/sirupsen/logrus v1.3.0 // indirect
|
||||
github.com/spf13/cobra v0.0.3
|
||||
github.com/spf13/pflag v1.0.3 // indirect
|
||||
github.com/stretchr/testify v1.2.2
|
||||
|
|
@ -32,9 +39,12 @@ require (
|
|||
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd // indirect
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.3.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
k8s.io/api v0.0.0-20190202010724-74b699b93c15
|
||||
k8s.io/apiextensions-apiserver v0.0.0-20190313122605-80ebb0f65ac1 // indirect
|
||||
k8s.io/apimachinery v0.0.0-20190207091153-095b9d203467
|
||||
k8s.io/apiserver v0.0.0-20190313120755-39e839dff034 // indirect
|
||||
k8s.io/cli-runtime v0.0.0-20190207094101-a32b78e5dd0a
|
||||
k8s.io/client-go v10.0.0+incompatible
|
||||
k8s.io/klog v0.1.0 // indirect
|
||||
|
|
|
|||
24
go.sum
24
go.sum
|
|
@ -7,6 +7,7 @@ git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGy
|
|||
git.apache.org/thrift.git v0.0.0-20181218151757-9b75e4fe745a/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/Azure/go-autorest v11.4.0+incompatible h1:z3Yr6KYqs0nhSNwqGXEBpWK977hxVqsLv2n9PVYcixY=
|
||||
github.com/Azure/go-autorest v11.4.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/census-instrumentation/opencensus-proto v0.1.0-0.20181214143942-ba49f56771b8 h1:gUqsFVdUKoRHNg8fkFd8gB5OOEa/g5EwlAHznb4zjbI=
|
||||
github.com/census-instrumentation/opencensus-proto v0.1.0-0.20181214143942-ba49f56771b8/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
|
|
@ -17,6 +18,8 @@ github.com/derailed/tview v0.1.3 h1:2/Rz0Sdfg3tepSKt4yCcY2g8IlRtPTrA4UYIQJZs6DI=
|
|||
github.com/derailed/tview v0.1.3/go.mod h1:WRYVfgb2PBMLZ/muaSpOc/4H4fYsOPnHOaGnBoJ+hGE=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/evanphx/json-patch v4.1.0+incompatible h1:K1MDoo4AZ4wU0GIU/fPmtZg7VpzLjCxu+UwBD1FvwOc=
|
||||
github.com/evanphx/json-patch v4.1.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
|
||||
|
|
@ -32,6 +35,8 @@ github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
|
|||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
|
|
@ -43,6 +48,8 @@ github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
|||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g=
|
||||
github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/gophercloud/gophercloud v0.0.0-20190206201033-83b528acebb4 h1:wAcVWwS69gs5c6cFkCa/ns/eaL2gC761nF8Ugvd1dGw=
|
||||
|
|
@ -52,6 +59,8 @@ github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:Fecb
|
|||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.6.2 h1:8KyC64BiO8ndiGHY5DlFWWdangUPC9QHPakFRre/Ud0=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
|
||||
|
|
@ -72,6 +81,7 @@ github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08 h1:5MnxBC1
|
|||
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
|
||||
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
|
@ -81,8 +91,12 @@ github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
|
|||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/petergtz/pegomock v0.0.0-20181206220228-b113d17a7e81 h1:MhSbvsIs4KvpPYr4taOvb6j+r9VNbj/08AfjsKi+Ui0=
|
||||
|
|
@ -90,12 +104,16 @@ github.com/petergtz/pegomock v0.0.0-20181206220228-b113d17a7e81/go.mod h1:nuBLWZ
|
|||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740=
|
||||
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.0.0-20181218105931-67670fe90761 h1:z6tvbDJ5OLJ48FFmnksv04a78maSTRBUIhkdHYV5Y98=
|
||||
github.com/prometheus/common v0.0.0-20181218105931-67670fe90761/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE=
|
||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/rivo/tview v0.0.0-20190213202703-b373355e9db4/go.mod h1:J4W+hErFfITUbyFAEXizpmkuxX7ZN56dopxHB4XQhMw=
|
||||
github.com/rs/zerolog v1.12.0 h1:aqZ1XRadoS8IBknR5IDFvGzbHly1X9ApIqOroooQF/c=
|
||||
|
|
@ -167,6 +185,8 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
|||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/square/go-jose.v2 v2.3.0 h1:nLzhkFyl5bkblqYBoiWJUt5JkWOzmiaBtCxdJAqJd3U=
|
||||
gopkg.in/square/go-jose.v2 v2.3.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
@ -176,8 +196,12 @@ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWh
|
|||
honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/api v0.0.0-20190202010724-74b699b93c15 h1:AoUGjnJ3PJMFz+Rkp4lx3X+6mPUnY1MESJhbUSGX+pc=
|
||||
k8s.io/api v0.0.0-20190202010724-74b699b93c15/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
|
||||
k8s.io/apiextensions-apiserver v0.0.0-20190313122605-80ebb0f65ac1 h1:YfQqwXg6zropY1zGFmoKq/XlXW283XNmYoHS6lSOHcw=
|
||||
k8s.io/apiextensions-apiserver v0.0.0-20190313122605-80ebb0f65ac1/go.mod h1:IxkesAMoaCRoLrPJdZNZUQp9NfZnzqaVzLhb2VEQzXE=
|
||||
k8s.io/apimachinery v0.0.0-20190207091153-095b9d203467 h1:zmz9UYvvXrK/B8EDqFuqreJEaXbIWdzEkNgWrN/Cd3o=
|
||||
k8s.io/apimachinery v0.0.0-20190207091153-095b9d203467/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
|
||||
k8s.io/apiserver v0.0.0-20190313120755-39e839dff034 h1:I/bl2Ni4Cn6bsjPIIZZCiuAodcZgRijgkXm/2Z2EbDg=
|
||||
k8s.io/apiserver v0.0.0-20190313120755-39e839dff034/go.mod h1:6bqaTSOSJavUIXUtfaR9Os9JtTCm8ZqH2SUl2S60C4w=
|
||||
k8s.io/cli-runtime v0.0.0-20190207094101-a32b78e5dd0a h1:MrGQxLLZ09Bl5hYYU9VlKnhY60bpPlYd9yXOPnxkdc0=
|
||||
k8s.io/cli-runtime v0.0.0-20190207094101-a32b78e5dd0a/go.mod h1:qWnH3/b8sp/l7EvlDh7ulDU3UWA4P4N1NFbEEP791tM=
|
||||
k8s.io/client-go v10.0.0+incompatible h1:F1IqCqw7oMBzDkqlcBymRq1450wD0eNqLE9jzUrIi34=
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ func TestConfigSaveFile(t *testing.T) {
|
|||
cfg.Load("test_assets/k9s.yml")
|
||||
cfg.K9s.RefreshRate = 100
|
||||
cfg.K9s.LogBufferSize = 500
|
||||
cfg.K9s.LogRequestSize = 100
|
||||
cfg.K9s.CurrentContext = "blee"
|
||||
cfg.K9s.CurrentCluster = "blee"
|
||||
cfg.Validate()
|
||||
|
|
@ -190,6 +191,7 @@ func setup(t *testing.T) {
|
|||
var expectedConfig = `k9s:
|
||||
refreshRate: 100
|
||||
logBufferSize: 500
|
||||
logRequestSize: 100
|
||||
currentContext: blee
|
||||
currentCluster: blee
|
||||
clusters:
|
||||
|
|
@ -227,6 +229,7 @@ var expectedConfig = `k9s:
|
|||
var resetConfig = `k9s:
|
||||
refreshRate: 2
|
||||
logBufferSize: 200
|
||||
logRequestSize: 200
|
||||
currentContext: blee
|
||||
currentCluster: blee
|
||||
clusters:
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
package config
|
||||
|
||||
const (
|
||||
defaultRefreshRate = 2
|
||||
defaultLogBufferSize = 200
|
||||
defaultRefreshRate = 2
|
||||
defaultLogRequestSize = 200
|
||||
defaultLogBufferSize = 1000
|
||||
)
|
||||
|
||||
// K9s tracks K9s configuration options.
|
||||
type K9s struct {
|
||||
RefreshRate int `yaml:"refreshRate"`
|
||||
LogBufferSize int `yaml:"logBufferSize"`
|
||||
LogRequestSize int `yaml:"logRequestSize"`
|
||||
CurrentContext string `yaml:"currentContext"`
|
||||
CurrentCluster string `yaml:"currentCluster"`
|
||||
Clusters map[string]*Cluster `yaml:"clusters,omitempty"`
|
||||
|
|
@ -18,10 +20,11 @@ type K9s struct {
|
|||
// NewK9s create a new K9s configuration.
|
||||
func NewK9s() *K9s {
|
||||
return &K9s{
|
||||
RefreshRate: defaultRefreshRate,
|
||||
LogBufferSize: defaultLogBufferSize,
|
||||
Clusters: map[string]*Cluster{},
|
||||
Aliases: map[string]string{},
|
||||
RefreshRate: defaultRefreshRate,
|
||||
LogBufferSize: defaultLogBufferSize,
|
||||
LogRequestSize: defaultLogRequestSize,
|
||||
Clusters: map[string]*Cluster{},
|
||||
Aliases: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,6 +54,10 @@ func (k *K9s) Validate(ks KubeSettings) {
|
|||
k.LogBufferSize = defaultLogBufferSize
|
||||
}
|
||||
|
||||
if k.LogRequestSize <= 0 {
|
||||
k.LogRequestSize = defaultLogRequestSize
|
||||
}
|
||||
|
||||
if k.Clusters == nil {
|
||||
k.Clusters = map[string]*Cluster{}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ func TestK9sValidate(t *testing.T) {
|
|||
c.Validate(ksMock)
|
||||
|
||||
assert.Equal(t, 2, c.RefreshRate)
|
||||
assert.Equal(t, 200, c.LogBufferSize)
|
||||
assert.Equal(t, 1000, c.LogBufferSize)
|
||||
assert.Equal(t, 200, c.LogRequestSize)
|
||||
assert.Equal(t, "ctx1", c.CurrentContext)
|
||||
assert.Equal(t, "c1", c.CurrentCluster)
|
||||
assert.Equal(t, 1, len(c.Clusters))
|
||||
|
|
@ -40,7 +41,8 @@ func TestK9sValidateBlank(t *testing.T) {
|
|||
c.Validate(ksMock)
|
||||
|
||||
assert.Equal(t, 2, c.RefreshRate)
|
||||
assert.Equal(t, 200, c.LogBufferSize)
|
||||
assert.Equal(t, 1000, c.LogBufferSize)
|
||||
assert.Equal(t, 200, c.LogRequestSize)
|
||||
assert.Equal(t, "ctx1", c.CurrentContext)
|
||||
assert.Equal(t, "c1", c.CurrentCluster)
|
||||
assert.Equal(t, 1, len(c.Clusters))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
k9s:
|
||||
refreshRate: 2
|
||||
logBufferSize: 200
|
||||
logRequestSize: 200
|
||||
currentContext: minikube
|
||||
currentCluster: minikube
|
||||
clusters:
|
||||
|
|
@ -8,21 +9,21 @@ k9s:
|
|||
namespace:
|
||||
active: kube-system
|
||||
favorites:
|
||||
- default
|
||||
- kube-public
|
||||
- istio-system
|
||||
- all
|
||||
- kube-system
|
||||
- default
|
||||
- kube-public
|
||||
- istio-system
|
||||
- all
|
||||
- kube-system
|
||||
view:
|
||||
active: ctx
|
||||
fred:
|
||||
namespace:
|
||||
active: default
|
||||
favorites:
|
||||
- default
|
||||
- kube-public
|
||||
- istio-system
|
||||
- all
|
||||
- kube-system
|
||||
- default
|
||||
- kube-public
|
||||
- istio-system
|
||||
- all
|
||||
- kube-system
|
||||
view:
|
||||
active: po
|
||||
active: po
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
package k8s
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
authorizationv1 "k8s.io/api/authorization/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
// CanIAccess checks if user has access to a certain resource.
|
||||
func CanIAccess(ns, verb, name, resURL string) bool {
|
||||
_, gr := schema.ParseResourceArg(strings.ToLower(resURL))
|
||||
sar := &authorizationv1.SelfSubjectAccessReview{
|
||||
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
|
||||
ResourceAttributes: &authorizationv1.ResourceAttributes{
|
||||
Namespace: ns,
|
||||
Verb: verb,
|
||||
Group: gr.Group,
|
||||
Resource: gr.Resource,
|
||||
Subresource: "",
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
auth, err := kubernetes.NewForConfig(conn.restConfigOrDie())
|
||||
if err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
response, err := auth.AuthorizationV1().SelfSubjectAccessReviews().Create(sar)
|
||||
if err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return response.Status.Allowed
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package k8s
|
||||
|
||||
import "github.com/rs/zerolog/log"
|
||||
|
||||
// Cluster represents a Kubernetes cluster.
|
||||
type Cluster struct{}
|
||||
|
||||
|
|
@ -12,6 +14,7 @@ func NewCluster() *Cluster {
|
|||
func (c *Cluster) Version() (string, error) {
|
||||
rev, err := conn.dialOrDie().Discovery().ServerVersion()
|
||||
if err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
return "", err
|
||||
}
|
||||
return rev.GitVersion, nil
|
||||
|
|
@ -21,6 +24,7 @@ func (c *Cluster) Version() (string, error) {
|
|||
func (c *Cluster) ContextName() string {
|
||||
ctx, err := conn.config.CurrentContextName()
|
||||
if err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
return "N/A"
|
||||
}
|
||||
return ctx
|
||||
|
|
@ -30,6 +34,7 @@ func (c *Cluster) ContextName() string {
|
|||
func (c *Cluster) ClusterName() string {
|
||||
ctx, err := conn.config.CurrentClusterName()
|
||||
if err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
return "N/A"
|
||||
}
|
||||
return ctx
|
||||
|
|
@ -39,6 +44,7 @@ func (c *Cluster) ClusterName() string {
|
|||
func (c *Cluster) UserName() string {
|
||||
usr, err := conn.config.CurrentUserName()
|
||||
if err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
return "N/A"
|
||||
}
|
||||
return usr
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ type NamedContext struct {
|
|||
Context *api.Context
|
||||
}
|
||||
|
||||
// MustCurrentClusterName return the active cluster name.
|
||||
func (c *NamedContext) MustCurrentClusterName() string {
|
||||
cl, err := conn.config.CurrentClusterName()
|
||||
// MustCurrentContextName return the active context name.
|
||||
func (c *NamedContext) MustCurrentContextName() string {
|
||||
cl, err := conn.config.CurrentContextName()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,16 @@ package k8s
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/restmapper"
|
||||
)
|
||||
|
||||
// RestMapping holds k8s resource mapping
|
||||
|
|
@ -15,13 +22,92 @@ var RestMapping = &RestMapper{}
|
|||
type RestMapper struct{}
|
||||
|
||||
// Find a mapping given a resource name.
|
||||
func (*RestMapper) Find(res string) (*meta.RESTMapping, error) {
|
||||
func (*RestMapper) Find1(res string) (*meta.RESTMapping, error) {
|
||||
if m, ok := resMap[res]; ok {
|
||||
return m, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no mapping for resource %s", res)
|
||||
}
|
||||
|
||||
func (*RestMapper) ToRESTMapper() (meta.RESTMapper, error) {
|
||||
rc := conn.restConfigOrDie()
|
||||
|
||||
httpCacheDir := filepath.Join(mustHomeDir(), ".kube", "http-cache")
|
||||
discCacheDir := filepath.Join(mustHomeDir(), ".kube", "cache", "discovery", toHostDir(rc.Host))
|
||||
|
||||
disc, err := discovery.NewCachedDiscoveryClientForConfig(rc, discCacheDir, httpCacheDir, 10*time.Minute)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mapper := restmapper.NewDeferredDiscoveryRESTMapper(disc)
|
||||
expander := restmapper.NewShortcutExpander(mapper, disc)
|
||||
return expander, nil
|
||||
}
|
||||
|
||||
var toFileName = regexp.MustCompile(`[^(\w/\.)]`)
|
||||
|
||||
func toHostDir(host string) string {
|
||||
h := strings.Replace(strings.Replace(host, "https://", "", 1), "http://", "", 1)
|
||||
// now do a simple collapse of non-AZ09 characters. Collisions are possible but unlikely. Even if we do collide the problem is short lived
|
||||
return toFileName.ReplaceAllString(h, "_")
|
||||
}
|
||||
|
||||
func mustHomeDir() string {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return usr.HomeDir
|
||||
}
|
||||
|
||||
// ResourceFor produces a rest mapping from a given resource.
|
||||
// Support full res name ie deployment.v1.apps.
|
||||
func (r *RestMapper) ResourceFor(resourceArg string) (*meta.RESTMapping, error) {
|
||||
res, err := r.resourceFor(resourceArg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.toRESTMapping(res, resourceArg), nil
|
||||
}
|
||||
|
||||
func (r *RestMapper) resourceFor(resourceArg string) (schema.GroupVersionResource, error) {
|
||||
if resourceArg == "*" {
|
||||
return schema.GroupVersionResource{Resource: resourceArg}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
gvr schema.GroupVersionResource
|
||||
err error
|
||||
)
|
||||
|
||||
mapper, err := r.ToRESTMapper()
|
||||
if err != nil {
|
||||
return gvr, err
|
||||
}
|
||||
|
||||
fullGVR, gr := schema.ParseResourceArg(strings.ToLower(resourceArg))
|
||||
if fullGVR != nil {
|
||||
return mapper.ResourceFor(*fullGVR)
|
||||
}
|
||||
|
||||
gvr, err = mapper.ResourceFor(gr.WithVersion(""))
|
||||
if err != nil {
|
||||
if len(gr.Group) == 0 {
|
||||
return gvr, fmt.Errorf("the server doesn't have a resource type '%s'", gr.Resource)
|
||||
}
|
||||
return gvr, fmt.Errorf("the server doesn't have a resource type '%s' in group '%s'", gr.Resource, gr.Group)
|
||||
}
|
||||
return gvr, nil
|
||||
}
|
||||
|
||||
func (*RestMapper) toRESTMapping(gvr schema.GroupVersionResource, res string) *meta.RESTMapping {
|
||||
return &meta.RESTMapping{
|
||||
Resource: gvr,
|
||||
GroupVersionKind: schema.GroupVersionKind{Group: gvr.Group, Version: gvr.Version, Kind: res},
|
||||
Scope: RestMapping,
|
||||
}
|
||||
}
|
||||
|
||||
// Name protocol returns rest scope name.
|
||||
func (*RestMapper) Name() meta.RESTScopeName {
|
||||
return meta.RESTScopeNameNamespace
|
||||
|
|
@ -155,28 +241,3 @@ var resMap = map[string]*meta.RESTMapping{
|
|||
Scope: RestMapping,
|
||||
},
|
||||
}
|
||||
|
||||
// {Group: "certificates.k8s.io", Version: "v1beta1", Kind: "CertificateSigningRequest"}: {},
|
||||
// {Group: "certificates.k8s.io", Version: "v1beta1", Kind: "CertificateSigningRequestList"}: {},
|
||||
// {Group: "kubeadm.k8s.io", Version: "v1alpha1", Kind: "MasterConfiguration"}: {},
|
||||
// {Group: "extensions", Version: "v1beta1", Kind: "PodSecurityPolicy"}: {},
|
||||
// {Group: "extensions", Version: "v1beta1", Kind: "PodSecurityPolicyList"}: {},
|
||||
// {Group: "extensions", Version: "v1beta1", Kind: "NetworkPolicy"}: {},
|
||||
// {Group: "extensions", Version: "v1beta1", Kind: "NetworkPolicyList"}: {},
|
||||
// {Group: "policy", Version: "v1beta1", Kind: "PodSecurityPolicy"}: {},
|
||||
// {Group: "policy", Version: "v1beta1", Kind: "PodSecurityPolicyList"}: {},
|
||||
// {Group: "settings.k8s.io", Version: "v1alpha1", Kind: "PodPreset"}: {},
|
||||
// {Group: "settings.k8s.io", Version: "v1alpha1", Kind: "PodPresetList"}: {},
|
||||
// {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfiguration"}: {},
|
||||
// {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfigurationList"}: {},
|
||||
// {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "MutatingWebhookConfiguration"}: {},
|
||||
// {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "MutatingWebhookConfigurationList"}: {},
|
||||
// {Group: "auditregistration.k8s.io", Version: "v1alpha1", Kind: "AuditSink"}: {},
|
||||
// {Group: "auditregistration.k8s.io", Version: "v1alpha1", Kind: "AuditSinkList"}: {},
|
||||
// {Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicy"}: {},
|
||||
// {Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicyList"}: {},
|
||||
// {Group: "storage.k8s.io", Version: "v1beta1", Kind: "StorageClass"}: {},
|
||||
// {Group: "storage.k8s.io", Version: "v1beta1", Kind: "StorageClassList"}: {},
|
||||
// {Group: "storage.k8s.io", Version: "v1", Kind: "StorageClass"}: {},
|
||||
// {Group: "storage.k8s.io", Version: "v1", Kind: "StorageClassList"}: {},
|
||||
// {Group: "authentication.k8s.io", Version: "v1", Kind: "TokenRequest"}: {},
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import (
|
|||
"math"
|
||||
"path"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"github.com/rs/zerolog/log"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
metricsapi "k8s.io/metrics/pkg/apis/metrics"
|
||||
|
|
@ -37,8 +38,10 @@ func (m *MetricsServer) NodeMetrics() (Metric, error) {
|
|||
opts := metav1.ListOptions{}
|
||||
nn, err := conn.dialOrDie().CoreV1().Nodes().List(opts)
|
||||
if err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
return mx, err
|
||||
}
|
||||
|
||||
nods := make([]string, len(nn.Items))
|
||||
var maxCPU, maxMem float64
|
||||
for i, n := range nn.Items {
|
||||
|
|
@ -51,6 +54,7 @@ func (m *MetricsServer) NodeMetrics() (Metric, error) {
|
|||
|
||||
mm, err := m.getNodeMetrics()
|
||||
if err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
return mx, err
|
||||
}
|
||||
|
||||
|
|
@ -67,6 +71,7 @@ func (m *MetricsServer) NodeMetrics() (Metric, error) {
|
|||
CPU: fmt.Sprintf("%0.f%%", math.Round((cpu/maxCPU)*100)),
|
||||
Mem: fmt.Sprintf("%0.f%%", math.Round((mem/maxMem)*100)),
|
||||
}
|
||||
|
||||
return mx, nil
|
||||
}
|
||||
|
||||
|
|
@ -76,6 +81,7 @@ func (m *MetricsServer) PodMetrics() (map[string]Metric, error) {
|
|||
|
||||
mm, err := m.getPodMetrics()
|
||||
if err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
return mx, err
|
||||
}
|
||||
|
||||
|
|
@ -88,6 +94,7 @@ func (m *MetricsServer) PodMetrics() (map[string]Metric, error) {
|
|||
pa := path.Join(m.Namespace, m.Name)
|
||||
mx[pa] = Metric{CPU: fmt.Sprintf("%dm", cpu), Mem: fmt.Sprintf("%dMi", mem)}
|
||||
}
|
||||
|
||||
return mx, nil
|
||||
}
|
||||
|
||||
|
|
@ -97,6 +104,7 @@ func (m *MetricsServer) PerNodeMetrics(nn []v1.Node) (map[string]Metric, error)
|
|||
|
||||
mm, err := m.getNodeMetrics()
|
||||
if err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
return mx, err
|
||||
}
|
||||
|
||||
|
|
@ -117,6 +125,7 @@ func (m *MetricsServer) PerNodeMetrics(nn []v1.Node) (map[string]Metric, error)
|
|||
AvailMem: fmt.Sprintf("%dMi", amem.Value()/(1024*1024)),
|
||||
}
|
||||
}
|
||||
|
||||
return mx, nil
|
||||
}
|
||||
|
||||
|
|
@ -124,70 +133,86 @@ func (m *MetricsServer) getPodMetrics() (*metricsapi.PodMetricsList, error) {
|
|||
if conn.hasMetricsServer() {
|
||||
return m.podMetricsViaService()
|
||||
}
|
||||
selector := labels.Everything()
|
||||
|
||||
var mx *metricsapi.PodMetricsList
|
||||
conn, err := conn.heapsterDial()
|
||||
if err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
return mx, err
|
||||
}
|
||||
return conn.GetPodMetrics("", "", true, selector)
|
||||
|
||||
return conn.GetPodMetrics("", "", true, labels.Everything())
|
||||
}
|
||||
|
||||
func (m *MetricsServer) getNodeMetrics() (*metricsapi.NodeMetricsList, error) {
|
||||
if conn.hasMetricsServer() {
|
||||
return m.nodeMetricsViaService()
|
||||
}
|
||||
selector := labels.Everything()
|
||||
|
||||
var mx *metricsapi.NodeMetricsList
|
||||
conn, err := conn.heapsterDial()
|
||||
if err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
return mx, err
|
||||
}
|
||||
return conn.GetNodeMetrics("", selector.String())
|
||||
|
||||
return conn.GetNodeMetrics("", labels.Everything().String())
|
||||
}
|
||||
|
||||
func (*MetricsServer) nodeMetricsViaService() (*metricsapi.NodeMetricsList, error) {
|
||||
var mx *metricsapi.NodeMetricsList
|
||||
|
||||
clt, err := conn.mxsDial()
|
||||
if err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
return mx, err
|
||||
}
|
||||
|
||||
selector := labels.Everything()
|
||||
var versionedMetrics *metricsV1beta1api.NodeMetricsList
|
||||
mc := clt.Metrics()
|
||||
nm := mc.NodeMetricses()
|
||||
versionedMetrics, err = nm.List(metav1.ListOptions{LabelSelector: selector.String()})
|
||||
if err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metrics := &metricsapi.NodeMetricsList{}
|
||||
err = metricsV1beta1api.Convert_v1beta1_NodeMetricsList_To_metrics_NodeMetricsList(versionedMetrics, metrics, nil)
|
||||
if err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
func (*MetricsServer) podMetricsViaService() (*metricsapi.PodMetricsList, error) {
|
||||
var mx *metricsapi.PodMetricsList
|
||||
|
||||
clt, err := conn.mxsDial()
|
||||
if err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
return mx, err
|
||||
}
|
||||
|
||||
selector := labels.Everything()
|
||||
var versionedMetrics *metricsV1beta1api.PodMetricsList
|
||||
mc := clt.Metrics()
|
||||
nm := mc.PodMetricses("")
|
||||
versionedMetrics, err = nm.List(metav1.ListOptions{LabelSelector: selector.String()})
|
||||
if err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metrics := &metricsapi.PodMetricsList{}
|
||||
err = metricsV1beta1api.Convert_v1beta1_PodMetricsList_To_metrics_PodMetricsList(versionedMetrics, metrics, nil)
|
||||
if err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package k8s
|
||||
|
||||
import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
|
|
@ -12,13 +13,14 @@ func NewNamespace() Res {
|
|||
return &Namespace{}
|
||||
}
|
||||
|
||||
// Get a namespace.
|
||||
// Get a active namespace.
|
||||
func (*Namespace) Get(_, n string) (interface{}, error) {
|
||||
opts := metav1.GetOptions{}
|
||||
|
||||
return conn.dialOrDie().CoreV1().Namespaces().Get(n, opts)
|
||||
}
|
||||
|
||||
// List all namespaces on the cluster.
|
||||
// List all active namespaces on the cluster.
|
||||
func (*Namespace) List(_ string) (Collection, error) {
|
||||
opts := metav1.ListOptions{}
|
||||
|
||||
|
|
@ -29,7 +31,9 @@ func (*Namespace) List(_ string) (Collection, error) {
|
|||
|
||||
cc := make(Collection, len(rr.Items))
|
||||
for i, r := range rr.Items {
|
||||
cc[i] = r
|
||||
if r.Status.Phase == v1.NamespaceActive {
|
||||
cc[i] = r
|
||||
}
|
||||
}
|
||||
|
||||
return cc, nil
|
||||
|
|
@ -38,5 +42,6 @@ func (*Namespace) List(_ string) (Collection, error) {
|
|||
// Delete a namespace.
|
||||
func (*Namespace) Delete(_, n string) error {
|
||||
opts := metav1.DeleteOptions{}
|
||||
|
||||
return conn.dialOrDie().CoreV1().Namespaces().Delete(n, &opts)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package printer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Defines basic ANSI colors.
|
||||
const (
|
||||
ColorBlack = iota + 30
|
||||
ColorRed
|
||||
ColorGreen
|
||||
ColorYellow
|
||||
ColorBlue
|
||||
ColorMagenta
|
||||
ColorCyan
|
||||
ColorWhite
|
||||
|
||||
ColorBold = 1
|
||||
ColorDarkGray = 90
|
||||
)
|
||||
|
||||
// Colorize a string based on given color.
|
||||
func Colorize(s string, c int) string {
|
||||
return fmt.Sprintf("\x1b[%dm%s\x1b[0m", c, s)
|
||||
}
|
||||
|
|
@ -70,12 +70,13 @@ func (b *Base) List(ns string) (Columnars, error) {
|
|||
|
||||
// Describe a given resource.
|
||||
func (b *Base) Describe(kind, pa string) (string, error) {
|
||||
|
||||
ns, n := namespaced(pa)
|
||||
mapping, err := k8s.RestMapping.Find(kind)
|
||||
|
||||
mapping, err := k8s.RestMapping.Find1(kind)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
d, err := versioned.Describer(k8s.KubeConfig.Flags(), mapping)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ func (r *Context) Fields(ns string) Row {
|
|||
i := r.instance
|
||||
|
||||
name := i.Name
|
||||
if i.MustCurrentClusterName() == name {
|
||||
if i.MustCurrentContextName() == name {
|
||||
name += "*"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/derailed/tview"
|
||||
runewidth "github.com/mattn/go-runewidth"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/duration"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
|
|
@ -82,12 +83,8 @@ func Pad(s string, l int) string {
|
|||
}
|
||||
|
||||
// Truncate a string to the given l and suffix ellipsis if needed.
|
||||
func Truncate(s string, l int) string {
|
||||
if len(s) > l {
|
||||
fmat := "%." + strconv.Itoa(l) + "s%s"
|
||||
return fmt.Sprintf(fmat, s, string(tview.SemigraphicsHorizontalEllipsis))
|
||||
}
|
||||
return s
|
||||
func Truncate(str string, width int) string {
|
||||
return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis))
|
||||
}
|
||||
|
||||
func mapToStr(m map[string]string) (s string) {
|
||||
|
|
|
|||
|
|
@ -82,8 +82,8 @@ func TestTruncate(t *testing.T) {
|
|||
l int
|
||||
e string
|
||||
}{
|
||||
{"fred", 2, "fr…"},
|
||||
{"fred", 1, "f…"},
|
||||
{"fred", 3, "fr…"},
|
||||
{"fred", 2, "f…"},
|
||||
{"fred", 10, "fred"},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ const (
|
|||
|
||||
type aliasView struct {
|
||||
*tableView
|
||||
|
||||
current igniter
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
|
@ -32,9 +33,9 @@ func newAliasView(app *appView) *aliasView {
|
|||
v.sortFn = v.sorterFn
|
||||
v.currentNS = ""
|
||||
}
|
||||
v.actions[tcell.KeyEnter] = newKeyAction("Search", v.gotoCmd)
|
||||
v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd)
|
||||
v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd)
|
||||
v.actions[tcell.KeyEnter] = newKeyAction("Goto", v.gotoCmd, true)
|
||||
v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd, false)
|
||||
v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd, false)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
v.cancel = cancel
|
||||
|
|
@ -50,6 +51,7 @@ func newAliasView(app *appView) *aliasView {
|
|||
}
|
||||
}
|
||||
}(ctx)
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +75,7 @@ func (v *aliasView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
v.cmdBuff.reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
return v.backCmd(evt)
|
||||
}
|
||||
|
||||
|
|
@ -85,6 +88,7 @@ func (v *aliasView) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
if v.cmdBuff.isActive() {
|
||||
return v.filterCmd(evt)
|
||||
}
|
||||
|
||||
return evt
|
||||
}
|
||||
|
||||
|
|
@ -92,19 +96,22 @@ func (v *aliasView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
if v.cancel != nil {
|
||||
v.cancel()
|
||||
}
|
||||
|
||||
if v.cmdBuff.isActive() {
|
||||
v.cmdBuff.reset()
|
||||
} else {
|
||||
v.app.inject(v.current)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *aliasView) runCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
r, _ := v.GetSelection()
|
||||
if r > 0 {
|
||||
v.app.command.run(strings.TrimSpace(v.GetCell(r, 0).Text))
|
||||
v.app.gotoResource(strings.TrimSpace(v.GetCell(r, 0).Text), true)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -133,6 +140,7 @@ func (v *aliasView) hydrate() resource.TableData {
|
|||
Deltas: fields,
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,20 +34,21 @@ type (
|
|||
appView struct {
|
||||
*tview.Application
|
||||
|
||||
version string
|
||||
pages *tview.Pages
|
||||
content *tview.Pages
|
||||
flashView *flashView
|
||||
menuView *menuView
|
||||
infoView *infoView
|
||||
command *command
|
||||
focusGroup []tview.Primitive
|
||||
focusCurrent int
|
||||
focusChanged focusHandler
|
||||
cancel context.CancelFunc
|
||||
cmdBuff *cmdBuff
|
||||
cmdView *cmdView
|
||||
actions keyActions
|
||||
version string
|
||||
pages *tview.Pages
|
||||
content *tview.Pages
|
||||
flashView *flashView
|
||||
crumbsView *crumbsView
|
||||
menuView *menuView
|
||||
clusterInfoView *clusterInfoView
|
||||
command *command
|
||||
focusGroup []tview.Primitive
|
||||
focusCurrent int
|
||||
focusChanged focusHandler
|
||||
cancel context.CancelFunc
|
||||
cmdBuff *cmdBuff
|
||||
cmdView *cmdView
|
||||
actions keyActions
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -68,32 +69,35 @@ func NewApp() *appView {
|
|||
v.cmdView = newCmdView('🐶')
|
||||
v.command = newCommand(&v)
|
||||
v.flashView = newFlashView(v.Application, "Initializing...")
|
||||
v.infoView = newInfoView(&v)
|
||||
v.crumbsView = newCrumbsView(v.Application)
|
||||
v.clusterInfoView = newInfoView(&v)
|
||||
v.focusChanged = v.changedFocus
|
||||
v.SetInputCapture(v.keyboard)
|
||||
}
|
||||
|
||||
v.actions[KeyColon] = newKeyAction("Cmd", v.activateCmd)
|
||||
v.actions[tcell.KeyCtrlR] = newKeyAction("Redraw", v.redrawCmd)
|
||||
v.actions[KeyQ] = newKeyAction("Quit", v.quitCmd)
|
||||
v.actions[KeyHelp] = newKeyAction("Help", v.helpCmd)
|
||||
v.actions[tcell.KeyEscape] = newKeyAction("Exit Cmd", v.deactivateCmd)
|
||||
v.actions[tcell.KeyEnter] = newKeyAction("Goto", v.gotoCmd)
|
||||
v.actions[tcell.KeyBackspace2] = newKeyAction("Goto", v.eraseCmd)
|
||||
v.actions[tcell.KeyTab] = newKeyAction("Focus", v.focusCmd)
|
||||
v.actions[KeyColon] = newKeyAction("Cmd", v.activateCmd, false)
|
||||
v.actions[tcell.KeyCtrlR] = newKeyAction("Redraw", v.redrawCmd, false)
|
||||
v.actions[KeyQ] = newKeyAction("Quit", v.quitCmd, false)
|
||||
v.actions[KeyHelp] = newKeyAction("Help", v.helpCmd, false)
|
||||
v.actions[KeyA] = newKeyAction("Aliases", v.aliasCmd, true)
|
||||
v.actions[tcell.KeyEscape] = newKeyAction("Exit Cmd", v.deactivateCmd, false)
|
||||
v.actions[tcell.KeyEnter] = newKeyAction("Goto", v.gotoCmd, false)
|
||||
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false)
|
||||
v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false)
|
||||
v.actions[tcell.KeyTab] = newKeyAction("Focus", v.focusCmd, false)
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
func (a *appView) Init(v string, rate int, flags *genericclioptions.ConfigFlags) {
|
||||
a.version = v
|
||||
a.infoView.init()
|
||||
a.clusterInfoView.init()
|
||||
a.cmdBuff.addListener(a.cmdView)
|
||||
|
||||
header := tview.NewFlex()
|
||||
{
|
||||
header.SetDirection(tview.FlexColumn)
|
||||
header.AddItem(a.infoView, 55, 1, false)
|
||||
header.AddItem(a.clusterInfoView, 55, 1, false)
|
||||
header.AddItem(a.menuView, 0, 1, false)
|
||||
header.AddItem(logoView(), 26, 1, false)
|
||||
}
|
||||
|
|
@ -104,11 +108,12 @@ func (a *appView) Init(v string, rate int, flags *genericclioptions.ConfigFlags)
|
|||
main.AddItem(header, 7, 1, false)
|
||||
main.AddItem(a.cmdView, 1, 1, false)
|
||||
main.AddItem(a.content, 0, 10, true)
|
||||
main.AddItem(a.flashView, 2, 1, false)
|
||||
main.AddItem(a.crumbsView, 2, 1, false)
|
||||
main.AddItem(a.flashView, 1, 1, false)
|
||||
}
|
||||
|
||||
a.pages.AddPage("main", main, true, false)
|
||||
a.pages.AddPage("splash", NewSplash(a.version), true, true)
|
||||
a.pages.AddPage("splash", newSplash(a.version), true, true)
|
||||
a.SetRoot(a.pages, true)
|
||||
}
|
||||
|
||||
|
|
@ -167,9 +172,18 @@ func (a *appView) deactivateCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
return evt
|
||||
}
|
||||
|
||||
func (a *appView) prevCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if top, ok := a.command.previousCmd(); ok {
|
||||
log.Debug().Msgf("Previous command %s", top)
|
||||
a.gotoResource(top, false)
|
||||
return nil
|
||||
}
|
||||
return evt
|
||||
}
|
||||
|
||||
func (a *appView) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if a.cmdBuff.isActive() && !a.cmdBuff.empty() {
|
||||
a.command.run(a.cmdBuff.String())
|
||||
a.gotoResource(a.cmdBuff.String(), true)
|
||||
a.cmdBuff.reset()
|
||||
return nil
|
||||
}
|
||||
|
|
@ -197,6 +211,14 @@ func (a *appView) quitCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
|
||||
func (a *appView) helpCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if a.cmdView.inCmdMode() {
|
||||
return evt
|
||||
}
|
||||
a.inject(newHelpView(a))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *appView) aliasCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if a.cmdView.inCmdMode() {
|
||||
return evt
|
||||
}
|
||||
|
|
@ -212,6 +234,14 @@ func (a *appView) puntCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
return evt
|
||||
}
|
||||
|
||||
func (a *appView) gotoResource(res string, record bool) bool {
|
||||
valid := a.command.run(res)
|
||||
if valid && record {
|
||||
a.command.pushCmd(res)
|
||||
}
|
||||
return valid
|
||||
}
|
||||
|
||||
func (a *appView) showPage(p string) {
|
||||
a.pages.SwitchToPage(p)
|
||||
}
|
||||
|
|
@ -240,7 +270,7 @@ func (a *appView) cmdMode() bool {
|
|||
}
|
||||
|
||||
func (a *appView) refresh() {
|
||||
a.infoView.refresh()
|
||||
a.clusterInfoView.refresh()
|
||||
}
|
||||
|
||||
func (a *appView) flash(level flashLevel, m ...string) {
|
||||
|
|
@ -248,7 +278,7 @@ func (a *appView) flash(level flashLevel, m ...string) {
|
|||
}
|
||||
|
||||
func (a *appView) setHints(h hints) {
|
||||
a.menuView.setMenu(h)
|
||||
a.menuView.populateMenu(h)
|
||||
}
|
||||
|
||||
func logoView() tview.Primitive {
|
||||
|
|
|
|||
|
|
@ -7,17 +7,17 @@ import (
|
|||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type infoView struct {
|
||||
type clusterInfoView struct {
|
||||
*tview.Table
|
||||
|
||||
app *appView
|
||||
}
|
||||
|
||||
func newInfoView(app *appView) *infoView {
|
||||
return &infoView{app: app, Table: tview.NewTable()}
|
||||
func newInfoView(app *appView) *clusterInfoView {
|
||||
return &clusterInfoView{app: app, Table: tview.NewTable()}
|
||||
}
|
||||
|
||||
func (v *infoView) init() {
|
||||
func (v *clusterInfoView) init() {
|
||||
cluster := resource.NewCluster()
|
||||
|
||||
var row int
|
||||
|
|
@ -49,20 +49,22 @@ func (v *infoView) init() {
|
|||
v.refresh()
|
||||
}
|
||||
|
||||
func (*infoView) sectionCell(t string) *tview.TableCell {
|
||||
func (*clusterInfoView) sectionCell(t string) *tview.TableCell {
|
||||
c := tview.NewTableCell(t + ":")
|
||||
c.SetAlign(tview.AlignLeft)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (*infoView) infoCell(t string) *tview.TableCell {
|
||||
func (*clusterInfoView) infoCell(t string) *tview.TableCell {
|
||||
c := tview.NewTableCell(t)
|
||||
c.SetExpansion(2)
|
||||
c.SetTextColor(tcell.ColorOrange)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (v *infoView) refresh() {
|
||||
func (v *clusterInfoView) refresh() {
|
||||
var row int
|
||||
|
||||
cluster := resource.NewCluster()
|
||||
|
|
@ -77,7 +79,7 @@ func (v *infoView) refresh() {
|
|||
|
||||
mx, err := cluster.Metrics()
|
||||
if err != nil {
|
||||
log.Error().Err(err)
|
||||
log.Warn().Msgf("%s", err)
|
||||
return
|
||||
}
|
||||
c := v.GetCell(row, 1)
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package views
|
||||
|
||||
import "github.com/rs/zerolog/log"
|
||||
|
||||
const maxStackSize = 10
|
||||
|
||||
type cmdStack struct {
|
||||
index int
|
||||
stack []string
|
||||
}
|
||||
|
||||
func newCmdStack() *cmdStack {
|
||||
return &cmdStack{stack: make([]string, 0, maxStackSize)}
|
||||
}
|
||||
|
||||
func (s *cmdStack) push(cmd string) {
|
||||
if len(s.stack) == maxStackSize {
|
||||
s.stack = s.stack[1 : len(s.stack)-1]
|
||||
}
|
||||
s.stack = append(s.stack, cmd)
|
||||
log.Info().Msgf("Pushed %s %v", cmd, s.stack)
|
||||
}
|
||||
|
||||
func (s *cmdStack) pop() (string, bool) {
|
||||
if s.empty() {
|
||||
return "", false
|
||||
}
|
||||
log.Info().Msgf("Before Pop %v", s.stack)
|
||||
top := s.stack[len(s.stack)-1]
|
||||
s.stack = s.stack[:len(s.stack)-1]
|
||||
log.Info().Msgf("After Pop %v", s.stack)
|
||||
return top, true
|
||||
}
|
||||
|
||||
func (s *cmdStack) top() (string, bool) {
|
||||
if s.empty() {
|
||||
return "", false
|
||||
}
|
||||
log.Info().Msgf("Top %v -- %s", s.stack, s.stack[len(s.stack)-1])
|
||||
|
||||
return s.stack[len(s.stack)-1], true
|
||||
}
|
||||
|
||||
func (s *cmdStack) empty() bool {
|
||||
return len(s.stack) == 0
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zerolog.SetGlobalLevel(zerolog.FatalLevel)
|
||||
}
|
||||
|
||||
func TestCmdStackPushMax(t *testing.T) {
|
||||
s := newCmdStack()
|
||||
for i := 0; i < 20; i++ {
|
||||
s.push(fmt.Sprintf("cmd_%d", i))
|
||||
}
|
||||
top, ok := s.top()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "cmd_19", top)
|
||||
}
|
||||
|
||||
func TestCmdStackPop(t *testing.T) {
|
||||
type expect struct {
|
||||
val string
|
||||
ok bool
|
||||
}
|
||||
|
||||
uu := []struct {
|
||||
cmds []string
|
||||
popCount int
|
||||
e expect
|
||||
}{
|
||||
{[]string{}, 2, expect{"", false}},
|
||||
{[]string{"a", "b", "c"}, 2, expect{"a", true}},
|
||||
{[]string{"a", "b", "c"}, 1, expect{"b", true}},
|
||||
}
|
||||
|
||||
for _, u := range uu {
|
||||
s := newCmdStack()
|
||||
for _, v := range u.cmds {
|
||||
s.push(v)
|
||||
}
|
||||
for i := 0; i < u.popCount; i++ {
|
||||
s.pop()
|
||||
}
|
||||
top, ok := s.pop()
|
||||
assert.Equal(t, u.e.ok, ok)
|
||||
assert.Equal(t, u.e.val, top)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdStackEmpty(t *testing.T) {
|
||||
uu := []struct {
|
||||
cmds []string
|
||||
popCount int
|
||||
e bool
|
||||
}{
|
||||
{[]string{}, 0, true},
|
||||
{[]string{"a", "b", "c"}, 0, false},
|
||||
{[]string{"a", "b", "c"}, 3, true},
|
||||
}
|
||||
|
||||
for _, u := range uu {
|
||||
s := newCmdStack()
|
||||
for _, v := range u.cmds {
|
||||
s.push(v)
|
||||
}
|
||||
for i := 0; i < u.popCount; i++ {
|
||||
s.pop()
|
||||
}
|
||||
assert.Equal(t, u.e, s.empty())
|
||||
}
|
||||
}
|
||||
|
|
@ -8,43 +8,56 @@ import (
|
|||
)
|
||||
|
||||
type command struct {
|
||||
app *appView
|
||||
app *appView
|
||||
history *cmdStack
|
||||
}
|
||||
|
||||
func newCommand(app *appView) *command {
|
||||
return &command{app: app}
|
||||
return &command{app: app, history: newCmdStack()}
|
||||
}
|
||||
|
||||
func (c *command) pushCmd(cmd string) {
|
||||
c.history.push(cmd)
|
||||
c.app.crumbsView.update(c.history.stack)
|
||||
}
|
||||
|
||||
func (c *command) previousCmd() (string, bool) {
|
||||
c.history.pop()
|
||||
c.app.crumbsView.update(c.history.stack)
|
||||
return c.history.top()
|
||||
}
|
||||
|
||||
// DefaultCmd reset default command ie show pods.
|
||||
func (c *command) defaultCmd() {
|
||||
c.pushCmd(config.Root.ActiveView())
|
||||
c.run(config.Root.ActiveView())
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
||||
// Exec the command by showing associated display.
|
||||
func (c *command) run(cmd string) {
|
||||
func (c *command) run(cmd string) bool {
|
||||
var v igniter
|
||||
switch cmd {
|
||||
case "q", "quit":
|
||||
c.app.Stop()
|
||||
return
|
||||
return true
|
||||
case "?", "help", "alias":
|
||||
c.app.inject(newAliasView(c.app))
|
||||
return
|
||||
return true
|
||||
default:
|
||||
if res, ok := cmdMap[cmd]; ok {
|
||||
if res, ok := resourceViews()[cmd]; ok {
|
||||
v = res.viewFn(res.title, c.app, res.listFn(resource.DefaultNamespace), res.colorerFn)
|
||||
c.app.flash(flashInfo, "Viewing all "+res.title+"...")
|
||||
c.app.flash(flashInfo, fmt.Sprintf("Viewing %s in namespace %s...", res.title, config.Root.ActiveNamespace()))
|
||||
c.exec(cmd, v)
|
||||
return
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
res, ok := getCRDS()[cmd]
|
||||
res, ok := allCRDs()[cmd]
|
||||
if !ok {
|
||||
c.app.flash(flashWarn, fmt.Sprintf("Huh? `%s` command not found", cmd))
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
n := res.Plural
|
||||
|
|
@ -58,6 +71,7 @@ func (c *command) run(cmd string) {
|
|||
defaultColorer,
|
||||
)
|
||||
c.exec(cmd, v)
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *command) exec(cmd string, v igniter) {
|
||||
|
|
|
|||
|
|
@ -14,19 +14,33 @@ type contextView struct {
|
|||
|
||||
func newContextView(t string, app *appView, list resource.List, c colorerFn) resourceViewer {
|
||||
v := contextView{newResourceView(t, app, list, c).(*resourceView)}
|
||||
v.extraActionsFn = v.extraActions
|
||||
|
||||
v.switchPage("ctx")
|
||||
|
||||
{
|
||||
v.extraActionsFn = v.extraActions
|
||||
v.switchPage("ctx")
|
||||
}
|
||||
return &v
|
||||
}
|
||||
|
||||
func (v *contextView) useContext(evt *tcell.EventKey) *tcell.EventKey {
|
||||
func (v *contextView) extraActions(aa keyActions) {
|
||||
aa[tcell.KeyEnter] = newKeyAction("Switch", v.useCmd, true)
|
||||
}
|
||||
|
||||
func (v *contextView) useCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !v.rowSelected() {
|
||||
return evt
|
||||
}
|
||||
if err := v.useContext(v.selectedItem); err != nil {
|
||||
v.app.flash(flashWarn, err.Error())
|
||||
return evt
|
||||
}
|
||||
|
||||
ctx := strings.TrimSpace(v.selectedItem)
|
||||
v.app.gotoResource("po", true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *contextView) useContext(name string) error {
|
||||
ctx := strings.TrimSpace(name)
|
||||
if strings.HasSuffix(ctx, "*") {
|
||||
ctx = strings.TrimRight(ctx, "*")
|
||||
}
|
||||
|
|
@ -34,15 +48,12 @@ func (v *contextView) useContext(evt *tcell.EventKey) *tcell.EventKey {
|
|||
ctx = strings.TrimRight(ctx, "(𝜟)")
|
||||
}
|
||||
|
||||
err := v.list.Resource().(*resource.Context).Switch(ctx)
|
||||
if err != nil {
|
||||
v.app.flash(flashWarn, err.Error())
|
||||
return evt
|
||||
if err := v.list.Resource().(*resource.Context).Switch(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config.Root.Reset()
|
||||
config.Root.Save()
|
||||
|
||||
v.app.flash(flashInfo, "Switching context to", ctx)
|
||||
v.refresh()
|
||||
if tv, ok := v.GetPrimitive("ctx").(*tableView); ok {
|
||||
|
|
@ -50,7 +61,3 @@ func (v *contextView) useContext(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *contextView) extraActions(aa keyActions) {
|
||||
aa[KeyU] = newKeyAction("Use", v.useContext)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,5 +34,5 @@ func (v *cronJobView) trigger(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
|
||||
func (v *cronJobView) extraActions(aa keyActions) {
|
||||
aa[tcell.KeyCtrlT] = newKeyAction("Trigger", v.trigger)
|
||||
aa[tcell.KeyCtrlT] = newKeyAction("Trigger", v.trigger, true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
type crumbsView struct {
|
||||
*tview.TextView
|
||||
|
||||
app *tview.Application
|
||||
}
|
||||
|
||||
func newCrumbsView(app *tview.Application) *crumbsView {
|
||||
v := crumbsView{app: app, TextView: tview.NewTextView()}
|
||||
{
|
||||
v.SetTextColor(tcell.ColorAqua)
|
||||
v.SetTextAlign(tview.AlignLeft)
|
||||
v.SetBorderPadding(0, 0, 1, 1)
|
||||
v.SetDynamicColors(true)
|
||||
}
|
||||
return &v
|
||||
}
|
||||
|
||||
func (v *crumbsView) update(crumbs []string) {
|
||||
v.Clear()
|
||||
last, bgColor := len(crumbs)-1, "aqua"
|
||||
for i, c := range crumbs {
|
||||
if i == last {
|
||||
bgColor = "orange"
|
||||
}
|
||||
fmt.Fprintf(v, "[black:%s:b] <%s> [-:-:-] ", bgColor, c)
|
||||
}
|
||||
}
|
||||
|
|
@ -53,12 +53,13 @@ func newDetailsView(app *appView, backFn actionHandler) *detailsView {
|
|||
})
|
||||
}
|
||||
|
||||
v.actions[KeySlash] = newKeyAction("Search", v.activateCmd)
|
||||
v.actions[tcell.KeyEnter] = newKeyAction("Search", v.searchCmd)
|
||||
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd)
|
||||
v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.backCmd)
|
||||
v.actions[tcell.KeyTab] = newKeyAction("Next", v.nextCmd)
|
||||
v.actions[tcell.KeyBacktab] = newKeyAction("Previous", v.prevCmd)
|
||||
// v.actions[KeySlash] = newKeyAction("Search", v.activateCmd)
|
||||
// v.actions[tcell.KeyEnter] = newKeyAction("Search", v.searchCmd)
|
||||
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false)
|
||||
v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false)
|
||||
v.actions[tcell.KeyEscape] = newKeyAction("Back", v.backCmd, true)
|
||||
v.actions[tcell.KeyTab] = newKeyAction("Next Match", v.nextCmd, false)
|
||||
v.actions[tcell.KeyBacktab] = newKeyAction("Previous Match", v.prevCmd, false)
|
||||
|
||||
return &v
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
|
@ -34,12 +35,12 @@ type (
|
|||
)
|
||||
|
||||
func newFlashView(app *tview.Application, m string) *flashView {
|
||||
var f flashView
|
||||
f := flashView{app: app, TextView: tview.NewTextView()}
|
||||
{
|
||||
f = flashView{app: app, TextView: tview.NewTextView()}
|
||||
f.SetTextColor(tcell.ColorAqua)
|
||||
f.SetTextAlign(tview.AlignLeft)
|
||||
f.SetBorderPadding(0, 0, 1, 1)
|
||||
f.SetText(m)
|
||||
}
|
||||
return &f
|
||||
}
|
||||
|
|
@ -48,14 +49,17 @@ func (f *flashView) setMessage(level flashLevel, msg ...string) {
|
|||
if f.cancel != nil {
|
||||
f.cancel()
|
||||
}
|
||||
|
||||
var ctx context.Context
|
||||
{
|
||||
ctx, f.cancel = context.WithTimeout(context.TODO(), flashDelay*time.Second)
|
||||
go func(ctx context.Context) {
|
||||
_, _, width, _ := f.GetRect()
|
||||
if width <= 15 {
|
||||
width = 100
|
||||
}
|
||||
m := strings.Join(msg, " ")
|
||||
f.SetTextColor(flashColor(level))
|
||||
f.SetText(flashEmoji(level) + " " + m)
|
||||
f.SetText(resource.Truncate(flashEmoji(level)+" "+m, width-3))
|
||||
f.app.Draw()
|
||||
for {
|
||||
select {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
helpTitle = "Help"
|
||||
helpTitleFmt = " [aqua::b]%s "
|
||||
)
|
||||
|
||||
type helpView struct {
|
||||
*tview.TextView
|
||||
|
||||
app *appView
|
||||
current igniter
|
||||
actions keyActions
|
||||
}
|
||||
|
||||
func newHelpView(app *appView) *helpView {
|
||||
v := helpView{TextView: tview.NewTextView(), app: app, actions: make(keyActions)}
|
||||
{
|
||||
v.SetTextColor(tcell.ColorAqua)
|
||||
v.SetBorder(true)
|
||||
v.SetBorderPadding(0, 0, 1, 1)
|
||||
v.SetDynamicColors(true)
|
||||
v.SetInputCapture(v.keyboard)
|
||||
v.current = app.content.GetPrimitive("main").(igniter)
|
||||
}
|
||||
v.actions[tcell.KeyEsc] = newKeyAction("Back", v.backCmd, true)
|
||||
v.actions[tcell.KeyEnter] = newKeyAction("Back", v.backCmd, false)
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
func (v *helpView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
key := evt.Key()
|
||||
if key == tcell.KeyRune {
|
||||
key = tcell.Key(evt.Rune())
|
||||
}
|
||||
|
||||
if a, ok := v.actions[key]; ok {
|
||||
log.Debug().Msgf(">> TableView handled %s", tcell.KeyNames[key])
|
||||
return a.action(evt)
|
||||
}
|
||||
return evt
|
||||
}
|
||||
|
||||
func (v *helpView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
v.app.inject(v.current)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *helpView) init(_ context.Context, _ string) {
|
||||
v.resetTitle()
|
||||
|
||||
type helpItem struct {
|
||||
key, description string
|
||||
}
|
||||
|
||||
general := []helpItem{
|
||||
{":<cmd>", "Command mode"},
|
||||
{"/<term>", "Filter mode"},
|
||||
{"esc", "Clear filter"},
|
||||
{"tab", "Next term match"},
|
||||
{"backtab", "Previous term match"},
|
||||
{"Ctrl-r", "Refresh"},
|
||||
{"p", "Previous resource view"},
|
||||
{"q", "Quit"},
|
||||
}
|
||||
fmt.Fprintf(v, "🏠 [aqua::b]%s\n", "General")
|
||||
for _, h := range general {
|
||||
fmt.Fprintf(v, "[pink::b]%9s [gray::]%s\n", h.key, h.description)
|
||||
}
|
||||
|
||||
navigation := []helpItem{
|
||||
{"g", "Goto Top"},
|
||||
{"G", "Goto Bottom"},
|
||||
{"b", "Page Down"},
|
||||
{"f", "Page Up"},
|
||||
{"l", "Left"},
|
||||
{"h", "Right"},
|
||||
{"k", "Up"},
|
||||
{"j", "Down"},
|
||||
}
|
||||
fmt.Fprintln(v)
|
||||
fmt.Fprintf(v, "🖲 [aqua::b]%s\n", "View Navigation")
|
||||
for _, h := range navigation {
|
||||
fmt.Fprintf(v, "[pink::b]%9s [gray::]%s\n", h.key, h.description)
|
||||
}
|
||||
|
||||
views := []helpItem{
|
||||
{"?", "Help"},
|
||||
{"a", "Aliases view"},
|
||||
}
|
||||
fmt.Fprintln(v)
|
||||
fmt.Fprintf(v, "️️⁉️ [aqua::b]%s\n", "Help")
|
||||
for _, h := range views {
|
||||
fmt.Fprintf(v, "[pink::b]%9s [gray::]%s\n", h.key, h.description)
|
||||
}
|
||||
|
||||
v.app.setHints(v.hints())
|
||||
}
|
||||
|
||||
func (v *helpView) hints() hints {
|
||||
return v.actions.toHints()
|
||||
}
|
||||
|
||||
func (v *helpView) getTitle() string {
|
||||
return helpTitle
|
||||
}
|
||||
|
||||
func (v *helpView) resetTitle() {
|
||||
v.SetTitle(fmt.Sprintf(helpTitleFmt, helpTitle))
|
||||
}
|
||||
|
|
@ -64,5 +64,5 @@ func (v *jobView) logs(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
|
||||
func (v *jobView) extraActions(aa keyActions) {
|
||||
aa[KeyL] = newKeyAction("Logs", v.logs)
|
||||
aa[KeyL] = newKeyAction("Logs", v.logs, true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/tview"
|
||||
)
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ func newLogView(title string, parent loggable) *logView {
|
|||
v.SetDynamicColors(true)
|
||||
v.SetWrap(true)
|
||||
v.setTitle(parent.getSelection())
|
||||
v.SetMaxBuffer(config.Root.K9s.LogBufferSize)
|
||||
}
|
||||
v.ansiWriter = tview.ANSIWriter(v)
|
||||
return &v
|
||||
|
|
|
|||
|
|
@ -35,12 +35,12 @@ func newLogsView(parent loggable) *logsView {
|
|||
containers: []string{},
|
||||
}
|
||||
v.setActions(keyActions{
|
||||
tcell.KeyEscape: {description: "Back", action: v.back},
|
||||
KeyC: {description: "Clear", action: v.clearLogs},
|
||||
KeyG: {description: "Top", action: v.top},
|
||||
KeyShiftG: {description: "Bottom", action: v.bottom},
|
||||
KeyF: {description: "Up", action: v.pageUp},
|
||||
KeyB: {description: "Down", action: v.pageDown},
|
||||
tcell.KeyEscape: {description: "Back", action: v.back, visible: true},
|
||||
KeyC: {description: "Clear", action: v.clearLogs, visible: true},
|
||||
KeyG: {description: "Top", action: v.top, visible: false},
|
||||
KeyShiftG: {description: "Bottom", action: v.bottom, visible: false},
|
||||
KeyF: {description: "Up", action: v.pageUp, visible: false},
|
||||
KeyB: {description: "Down", action: v.pageDown, visible: false},
|
||||
})
|
||||
v.SetInputCapture(v.keyboard)
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ func (v *logsView) setActions(aa keyActions) {
|
|||
func (v *logsView) hints() hints {
|
||||
if len(v.containers) > 1 {
|
||||
for i, c := range v.containers {
|
||||
v.actions[tcell.Key(numKeys[i+1])] = newKeyAction(c, nil)
|
||||
v.actions[tcell.Key(numKeys[i+1])] = newKeyAction(c, nil, true)
|
||||
}
|
||||
}
|
||||
return v.actions.toHints()
|
||||
|
|
@ -163,7 +163,7 @@ func (v *logsView) doLoad(path, co string) error {
|
|||
if !ok {
|
||||
return fmt.Errorf("Resource %T is not tailable", v.parent.getList().Resource)
|
||||
}
|
||||
maxBuff := int64(config.Root.K9s.LogBufferSize)
|
||||
maxBuff := int64(config.Root.K9s.LogRequestSize)
|
||||
cancelFn, err := res.Logs(c, ns, po, co, maxBuff, false)
|
||||
if err != nil {
|
||||
cancelFn()
|
||||
|
|
@ -200,14 +200,18 @@ func (v *logsView) bottom(*tcell.EventKey) *tcell.EventKey {
|
|||
|
||||
func (v *logsView) pageUp(*tcell.EventKey) *tcell.EventKey {
|
||||
if p := v.CurrentPage(); p != nil {
|
||||
p.Item.(*logView).PageUp()
|
||||
if p.Item.(*logView).PageUp() {
|
||||
v.parent.appView().flash(flashInfo, "Reached Top ...")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *logsView) pageDown(*tcell.EventKey) *tcell.EventKey {
|
||||
if p := v.CurrentPage(); p != nil {
|
||||
p.Item.(*logView).PageDown()
|
||||
if p.Item.(*logView).PageDown() {
|
||||
v.parent.appView().flash(flashInfo, "Reached Bottom ...")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,17 +14,16 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
menuSepFmt = " [dodgerblue::b]%-9s [white::d]%s "
|
||||
menuSepFmt = " [dodgerblue::b]%-%ds [white::d]%s "
|
||||
menuIndexFmt = " [fuchsia::b]<%d> [white::d]%s "
|
||||
maxRows = 6
|
||||
colLen = 20
|
||||
maxRows = 7
|
||||
)
|
||||
|
||||
var menuRX = regexp.MustCompile(`\d`)
|
||||
|
||||
type (
|
||||
hint struct {
|
||||
mnemonic, display string
|
||||
mnemonic, description string
|
||||
}
|
||||
hints []hint
|
||||
|
||||
|
|
@ -53,7 +52,7 @@ func (h hints) Less(i, j int) bool {
|
|||
if err1 != nil && err2 == nil {
|
||||
return false
|
||||
}
|
||||
return strings.Compare(h[i].mnemonic, h[j].mnemonic) < 0
|
||||
return strings.Compare(h[i].description, h[j].description) < 0
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
|
@ -63,12 +62,13 @@ type (
|
|||
keyAction struct {
|
||||
description string
|
||||
action actionHandler
|
||||
visible bool
|
||||
}
|
||||
keyActions map[tcell.Key]keyAction
|
||||
)
|
||||
|
||||
func newKeyAction(d string, a actionHandler) keyAction {
|
||||
return keyAction{description: d, action: a}
|
||||
func newKeyAction(d string, a actionHandler, display bool) keyAction {
|
||||
return keyAction{description: d, action: a, visible: display}
|
||||
}
|
||||
|
||||
func newMenuView() *menuView {
|
||||
|
|
@ -76,72 +76,21 @@ func newMenuView() *menuView {
|
|||
return &v
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
type menuView struct {
|
||||
*tview.Table
|
||||
}
|
||||
|
||||
func (v *menuView) setMenu(hh hints) {
|
||||
v.Clear()
|
||||
sort.Sort(hh)
|
||||
|
||||
var row, col int
|
||||
firstNS, firstCmd := true, true
|
||||
for _, h := range hh {
|
||||
isDigit := menuRX.MatchString(h.mnemonic)
|
||||
if isDigit && firstNS {
|
||||
row, col, firstNS = 0, 2, false
|
||||
}
|
||||
if !isDigit && firstCmd {
|
||||
row, col, firstCmd = 0, 0, false
|
||||
}
|
||||
c := tview.NewTableCell(v.item(h))
|
||||
v.SetCell(row, col, c)
|
||||
row++
|
||||
if row > maxRows {
|
||||
col++
|
||||
row = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (*menuView) toMnemonic(s string) string {
|
||||
return "<" + strings.ToLower(s) + ">"
|
||||
}
|
||||
|
||||
func (v *menuView) item(h hint) string {
|
||||
i, err := strconv.Atoi(h.mnemonic)
|
||||
if err == nil {
|
||||
return fmt.Sprintf(menuIndexFmt, i, resource.Truncate(h.display, 14))
|
||||
}
|
||||
return fmt.Sprintf(menuSepFmt, v.toMnemonic(h.mnemonic), h.display)
|
||||
}
|
||||
|
||||
func (a keyActions) skipKey(k tcell.Key) bool {
|
||||
switch k {
|
||||
case tcell.KeyBackspace2:
|
||||
fallthrough
|
||||
case tcell.KeyEnter:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a keyActions) toHints() hints {
|
||||
kk := make([]int, 0, len(a))
|
||||
for k := range a {
|
||||
if !a.skipKey(k) {
|
||||
for k, v := range a {
|
||||
if v.visible {
|
||||
kk = append(kk, int(k))
|
||||
}
|
||||
}
|
||||
sort.Ints(kk)
|
||||
|
||||
hh := make(hints, 0, len(a))
|
||||
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,
|
||||
display: a[tcell.Key(k)].description})
|
||||
mnemonic: name,
|
||||
description: a[tcell.Key(k)].description})
|
||||
} else {
|
||||
log.Error().Msgf("Unable to local KeyName for %#v", k)
|
||||
}
|
||||
|
|
@ -149,6 +98,85 @@ func (a keyActions) toHints() hints {
|
|||
return hh
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
type menuView struct {
|
||||
*tview.Table
|
||||
}
|
||||
|
||||
func (v *menuView) populateMenu(hh hints) {
|
||||
v.Clear()
|
||||
sort.Sort(hh)
|
||||
|
||||
t := v.buildMenuTable(hh)
|
||||
for row := 0; row < len(t); row++ {
|
||||
for col := 0; col < len(t[row]); col++ {
|
||||
if len(t[row][col]) == 0 {
|
||||
continue
|
||||
}
|
||||
c := tview.NewTableCell(t[row][col])
|
||||
v.SetCell(row, col, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *menuView) buildMenuTable(hh hints) [][]string {
|
||||
table := make([][]hint, maxRows+1)
|
||||
|
||||
colCount := (len(hh) / maxRows) + 1
|
||||
for row := 0; row < maxRows; row++ {
|
||||
table[row] = make([]hint, colCount)
|
||||
}
|
||||
|
||||
var row, col int
|
||||
firstNS, firstCmd := true, true
|
||||
maxKeys := make([]int, colCount)
|
||||
for _, h := range hh {
|
||||
isDigit := menuRX.MatchString(h.mnemonic)
|
||||
if isDigit && firstNS {
|
||||
row, col, firstNS = 0, col+1, false
|
||||
}
|
||||
if !isDigit && firstCmd {
|
||||
row, col, firstCmd = 0, 0, false
|
||||
}
|
||||
if maxKeys[col] < len(h.mnemonic) {
|
||||
maxKeys[col] = len(h.mnemonic)
|
||||
}
|
||||
table[row][col] = h
|
||||
row++
|
||||
if row >= maxRows {
|
||||
col++
|
||||
row = 0
|
||||
}
|
||||
}
|
||||
|
||||
strTable := make([][]string, maxRows+1)
|
||||
for r := 0; r < len(table); r++ {
|
||||
strTable[r] = make([]string, len(table[r]))
|
||||
}
|
||||
for row := range strTable {
|
||||
for col := range strTable[row] {
|
||||
strTable[row][col] = v.formatMenu(table[row][col], maxKeys[col])
|
||||
}
|
||||
}
|
||||
return strTable
|
||||
}
|
||||
|
||||
func (*menuView) toMnemonic(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
return "<" + strings.ToLower(s) + ">"
|
||||
}
|
||||
|
||||
func (v *menuView) formatMenu(h hint, size int) string {
|
||||
i, err := strconv.Atoi(h.mnemonic)
|
||||
if err == nil {
|
||||
return fmt.Sprintf(menuIndexFmt, i, resource.Truncate(h.description, 14))
|
||||
}
|
||||
menuFmt := " [dodgerblue::b]%-" + strconv.Itoa(size+2) + "s [white::d]%s "
|
||||
return fmt.Sprintf(menuFmt, v.toMnemonic(h.mnemonic), h.description)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Key mapping Constants
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"regexp"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
|
@ -31,21 +32,34 @@ func newNamespaceView(t string, app *appView, list resource.List, c colorerFn) r
|
|||
}
|
||||
|
||||
func (v *namespaceView) extraActions(aa keyActions) {
|
||||
aa[KeyU] = newKeyAction("Use", v.useNamespace)
|
||||
aa[tcell.KeyEnter] = newKeyAction("Switch", v.switchNsCmd, true)
|
||||
aa[KeyU] = newKeyAction("Use", v.useNsCmd, true)
|
||||
}
|
||||
|
||||
func (v *namespaceView) useNamespace(evt *tcell.EventKey) *tcell.EventKey {
|
||||
func (v *namespaceView) switchNsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !v.rowSelected() {
|
||||
return evt
|
||||
}
|
||||
ns := v.getSelectedItem()
|
||||
if err := config.Root.SetActiveNamespace(ns); err != nil {
|
||||
v.useNamespace(v.getSelectedItem())
|
||||
v.app.gotoResource("po", true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *namespaceView) useNsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !v.rowSelected() {
|
||||
return evt
|
||||
}
|
||||
v.useNamespace(v.getSelectedItem())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *namespaceView) useNamespace(name string) {
|
||||
if err := config.Root.SetActiveNamespace(name); err != nil {
|
||||
v.app.flash(flashErr, err.Error())
|
||||
} else {
|
||||
v.app.flash(flashInfo, fmt.Sprintf("Namespace %s is now active!", ns))
|
||||
v.app.flash(flashInfo, fmt.Sprintf("Namespace %s is now active!", name))
|
||||
}
|
||||
config.Root.Save()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *namespaceView) getSelectedItem() string {
|
||||
|
|
@ -58,10 +72,12 @@ func (*namespaceView) cleanser(s string) string {
|
|||
|
||||
func (v *namespaceView) decorate(data resource.TableData) resource.TableData {
|
||||
if _, ok := data.Rows[resource.AllNamespaces]; !ok {
|
||||
data.Rows[resource.AllNamespace] = &resource.RowEvent{
|
||||
Action: resource.Unchanged,
|
||||
Fields: resource.Row{resource.AllNamespace, "Active", "0"},
|
||||
Deltas: resource.Row{"", "", ""},
|
||||
if k8s.CanIAccess("", "list", "namespaces", "namespace.v1") {
|
||||
data.Rows[resource.AllNamespace] = &resource.RowEvent{
|
||||
Action: resource.Unchanged,
|
||||
Fields: resource.Row{resource.AllNamespace, "Active", "0"},
|
||||
Deltas: resource.Row{"", "", ""},
|
||||
}
|
||||
}
|
||||
}
|
||||
for k, v := range data.Rows {
|
||||
|
|
|
|||
|
|
@ -141,8 +141,8 @@ func (v *podView) showLogs(path, co string, previous bool) {
|
|||
}
|
||||
|
||||
func (v *podView) extraActions(aa keyActions) {
|
||||
aa[KeyL] = newKeyAction("Logs", v.logsCmd)
|
||||
aa[KeyS] = newKeyAction("Shell", v.shellCmd)
|
||||
aa[KeyL] = newKeyAction("Logs", v.logsCmd, true)
|
||||
aa[KeyS] = newKeyAction("Shell", v.shellCmd, true)
|
||||
}
|
||||
|
||||
func fetchContainers(l resource.List, po string, includeInit bool) ([]string, error) {
|
||||
|
|
|
|||
|
|
@ -20,203 +20,20 @@ type (
|
|||
}
|
||||
)
|
||||
|
||||
var cmdMap = map[string]resCmd{
|
||||
"cm": {
|
||||
title: "ConfigMaps",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewConfigMapList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"cr": {
|
||||
title: "ClusterRoles",
|
||||
api: "rbac.authorization.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewClusterRoleList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"crb": {
|
||||
title: "ClusterRoleBindings",
|
||||
api: "rbac.authorization.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewClusterRoleBindingList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"crd": {
|
||||
title: "CustomResourceDefinitions",
|
||||
api: "apiextensions.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewCRDList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"cron": {
|
||||
title: "CronJobs",
|
||||
api: "batch",
|
||||
viewFn: newCronJobView,
|
||||
listFn: resource.NewCronJobList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"ctx": {
|
||||
title: "Contexts",
|
||||
api: "core",
|
||||
viewFn: newContextView,
|
||||
listFn: resource.NewContextList,
|
||||
colorerFn: ctxColorer,
|
||||
},
|
||||
"ds": {
|
||||
title: "DaemonSets",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewDaemonSetList,
|
||||
colorerFn: dpColorer,
|
||||
},
|
||||
"dp": {
|
||||
title: "Deployments",
|
||||
api: "apps",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewDeploymentList,
|
||||
colorerFn: dpColorer,
|
||||
},
|
||||
"ep": {
|
||||
title: "EndPoints",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewEndpointsList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"ev": {
|
||||
title: "Events",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewEventList,
|
||||
colorerFn: evColorer,
|
||||
},
|
||||
"hpa": {
|
||||
title: "HorizontalPodAutoscalers",
|
||||
api: "autoscaling",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewHPAList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"ing": {
|
||||
title: "Ingress",
|
||||
api: "extensions",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewIngressList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"job": {
|
||||
title: "Jobs",
|
||||
api: "batch",
|
||||
viewFn: newJobView,
|
||||
listFn: resource.NewJobList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"no": {
|
||||
title: "Nodes",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewNodeList,
|
||||
colorerFn: nsColorer,
|
||||
},
|
||||
"ns": {
|
||||
title: "Namespaces",
|
||||
api: "core",
|
||||
viewFn: newNamespaceView,
|
||||
listFn: resource.NewNamespaceList,
|
||||
colorerFn: nsColorer,
|
||||
},
|
||||
"po": {
|
||||
title: "Pods",
|
||||
api: "core",
|
||||
viewFn: newPodView,
|
||||
listFn: resource.NewPodList,
|
||||
colorerFn: podColorer,
|
||||
},
|
||||
"pv": {
|
||||
title: "PersistentVolumes",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewPVList,
|
||||
colorerFn: pvColorer,
|
||||
},
|
||||
"pvc": {
|
||||
title: "PersistentVolumeClaims",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewPVCList,
|
||||
colorerFn: pvcColorer,
|
||||
},
|
||||
"rb": {
|
||||
title: "RoleBindings",
|
||||
api: "rbac.authorization.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewRoleBindingList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"rc": {
|
||||
title: "ReplicationControllers",
|
||||
api: "v1",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewReplicationControllerList,
|
||||
colorerFn: rsColorer,
|
||||
},
|
||||
"ro": {
|
||||
title: "Roles",
|
||||
api: "rbac.authorization.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewRoleList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"rs": {
|
||||
title: "ReplicaSets",
|
||||
api: "apps",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewReplicaSetList,
|
||||
colorerFn: rsColorer,
|
||||
},
|
||||
"sa": {
|
||||
title: "ServiceAccounts",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewServiceAccountList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"sec": {
|
||||
title: "Secrets",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewSecretList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"sts": {
|
||||
title: "StatefulSets",
|
||||
api: "apps",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewStatefulSetList,
|
||||
colorerFn: stsColorer,
|
||||
},
|
||||
"svc": {
|
||||
title: "Services",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewServiceList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
}
|
||||
|
||||
func helpCmds() map[string]resCmd {
|
||||
cmdMap := resourceViews()
|
||||
cmds := make(map[string]resCmd, len(cmdMap))
|
||||
for k, v := range cmdMap {
|
||||
cmds[k] = v
|
||||
}
|
||||
for k, v := range getCRDS() {
|
||||
for k, v := range allCRDs() {
|
||||
cmds[k] = resCmd{title: v.Kind, api: v.Group}
|
||||
}
|
||||
|
||||
return cmds
|
||||
}
|
||||
|
||||
func getCRDS() map[string]k8s.APIGroup {
|
||||
func allCRDs() map[string]k8s.APIGroup {
|
||||
m := map[string]k8s.APIGroup{}
|
||||
list := resource.NewCRDList(resource.AllNamespaces)
|
||||
ll, _ := list.Resource().List(resource.AllNamespaces)
|
||||
|
|
@ -248,5 +65,193 @@ func getCRDS() map[string]k8s.APIGroup {
|
|||
m[s] = grp
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func resourceViews() map[string]resCmd {
|
||||
return map[string]resCmd{
|
||||
"cm": {
|
||||
title: "ConfigMaps",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewConfigMapList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"cr": {
|
||||
title: "ClusterRoles",
|
||||
api: "rbac.authorization.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewClusterRoleList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"crb": {
|
||||
title: "ClusterRoleBindings",
|
||||
api: "rbac.authorization.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewClusterRoleBindingList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"crd": {
|
||||
title: "CustomResourceDefinitions",
|
||||
api: "apiextensions.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewCRDList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"cron": {
|
||||
title: "CronJobs",
|
||||
api: "batch",
|
||||
viewFn: newCronJobView,
|
||||
listFn: resource.NewCronJobList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"ctx": {
|
||||
title: "Contexts",
|
||||
api: "core",
|
||||
viewFn: newContextView,
|
||||
listFn: resource.NewContextList,
|
||||
colorerFn: ctxColorer,
|
||||
},
|
||||
"ds": {
|
||||
title: "DaemonSets",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewDaemonSetList,
|
||||
colorerFn: dpColorer,
|
||||
},
|
||||
"dp": {
|
||||
title: "Deployments",
|
||||
api: "apps",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewDeploymentList,
|
||||
colorerFn: dpColorer,
|
||||
},
|
||||
"ep": {
|
||||
title: "EndPoints",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewEndpointsList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"ev": {
|
||||
title: "Events",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewEventList,
|
||||
colorerFn: evColorer,
|
||||
},
|
||||
"hpa": {
|
||||
title: "HorizontalPodAutoscalers",
|
||||
api: "autoscaling",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewHPAList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"ing": {
|
||||
title: "Ingress",
|
||||
api: "extensions",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewIngressList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"job": {
|
||||
title: "Jobs",
|
||||
api: "batch",
|
||||
viewFn: newJobView,
|
||||
listFn: resource.NewJobList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"no": {
|
||||
title: "Nodes",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewNodeList,
|
||||
colorerFn: nsColorer,
|
||||
},
|
||||
"ns": {
|
||||
title: "Namespaces",
|
||||
api: "core",
|
||||
viewFn: newNamespaceView,
|
||||
listFn: resource.NewNamespaceList,
|
||||
colorerFn: nsColorer,
|
||||
},
|
||||
"po": {
|
||||
title: "Pods",
|
||||
api: "core",
|
||||
viewFn: newPodView,
|
||||
listFn: resource.NewPodList,
|
||||
colorerFn: podColorer,
|
||||
},
|
||||
"pv": {
|
||||
title: "PersistentVolumes",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewPVList,
|
||||
colorerFn: pvColorer,
|
||||
},
|
||||
"pvc": {
|
||||
title: "PersistentVolumeClaims",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewPVCList,
|
||||
colorerFn: pvcColorer,
|
||||
},
|
||||
"rb": {
|
||||
title: "RoleBindings",
|
||||
api: "rbac.authorization.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewRoleBindingList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"rc": {
|
||||
title: "ReplicationControllers",
|
||||
api: "v1",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewReplicationControllerList,
|
||||
colorerFn: rsColorer,
|
||||
},
|
||||
"ro": {
|
||||
title: "Roles",
|
||||
api: "rbac.authorization.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewRoleList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"rs": {
|
||||
title: "ReplicaSets",
|
||||
api: "apps",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewReplicaSetList,
|
||||
colorerFn: rsColorer,
|
||||
},
|
||||
"sa": {
|
||||
title: "ServiceAccounts",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewServiceAccountList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"sec": {
|
||||
title: "Secrets",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewSecretList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"sts": {
|
||||
title: "StatefulSets",
|
||||
api: "apps",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewStatefulSetList,
|
||||
colorerFn: stsColorer,
|
||||
},
|
||||
"svc": {
|
||||
title: "Services",
|
||||
api: "core",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewServiceList,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@ import (
|
|||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const noSelection = ""
|
||||
const (
|
||||
refreshDelay = 0.1
|
||||
noSelection = ""
|
||||
)
|
||||
|
||||
type (
|
||||
details interface {
|
||||
|
|
@ -77,7 +80,7 @@ func (v *resourceView) init(ctx context.Context, ns string) {
|
|||
v.selectedItem, v.selectedNS = noSelection, ns
|
||||
|
||||
go func(ctx context.Context) {
|
||||
initTick := 0.1
|
||||
initTick := refreshDelay
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
|
@ -169,8 +172,8 @@ func (v *resourceView) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
sel := v.getSelectedItem()
|
||||
raw, err := v.list.Resource().Describe(v.title, sel)
|
||||
if err != nil {
|
||||
v.app.flash(flashErr, "Unable to describeCmd this resource", err.Error())
|
||||
log.Error().Err(err)
|
||||
v.app.flash(flashErr, err.Error())
|
||||
log.Warn().Msg(err.Error())
|
||||
return evt
|
||||
}
|
||||
details := v.GetPrimitive("details").(*detailsView)
|
||||
|
|
@ -250,8 +253,6 @@ func (v *resourceView) doSwitchNamespace(ns string) {
|
|||
config.Root.Save()
|
||||
}
|
||||
|
||||
// Utils...
|
||||
|
||||
func (v *resourceView) refresh() {
|
||||
if _, ok := v.CurrentPage().Item.(*tableView); !ok {
|
||||
return
|
||||
|
|
@ -263,6 +264,7 @@ func (v *resourceView) refresh() {
|
|||
v.list.SetNamespace(v.selectedNS)
|
||||
}
|
||||
if err := v.list.Reconcile(); err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
v.app.flash(flashErr, err.Error())
|
||||
}
|
||||
data := v.list.Data()
|
||||
|
|
@ -272,7 +274,7 @@ func (v *resourceView) refresh() {
|
|||
v.getTV().update(data)
|
||||
v.selectItem(v.selectedRow, 0)
|
||||
v.refreshActions()
|
||||
v.app.infoView.refresh()
|
||||
v.app.clusterInfoView.refresh()
|
||||
v.app.Draw()
|
||||
}
|
||||
v.update.Unlock()
|
||||
|
|
@ -334,49 +336,51 @@ func (v *resourceView) refreshActions() {
|
|||
return
|
||||
}
|
||||
|
||||
nn, err := k8s.NewNamespace().List(resource.AllNamespaces)
|
||||
if err != nil {
|
||||
v.app.flash(flashErr, "Unable to retrieve namespaces", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if v.list.Namespaced() && !v.list.AllNamespaces() {
|
||||
if !config.InNSList(nn, v.list.GetNamespace()) {
|
||||
v.list.SetNamespace(resource.DefaultNamespace)
|
||||
}
|
||||
}
|
||||
|
||||
var nn []interface{}
|
||||
aa := make(keyActions)
|
||||
if v.list.Access(resource.NamespaceAccess) {
|
||||
v.namespaces = make(map[int]string, config.MaxFavoritesNS)
|
||||
for i, n := range config.Root.FavNamespaces() {
|
||||
aa[tcell.Key(numKeys[i])] = newKeyAction(n, v.switchNamespaceCmd)
|
||||
v.namespaces[i] = n
|
||||
if k8s.CanIAccess("", "list", "namespaces", "namespace.v1") {
|
||||
var err error
|
||||
nn, err = k8s.NewNamespace().List(resource.AllNamespaces)
|
||||
if err != nil {
|
||||
log.Warn().Msgf("%s", err)
|
||||
v.app.flash(flashErr, err.Error())
|
||||
}
|
||||
|
||||
if v.list.Namespaced() && !v.list.AllNamespaces() {
|
||||
if !config.InNSList(nn, v.list.GetNamespace()) {
|
||||
v.list.SetNamespace(resource.DefaultNamespace)
|
||||
}
|
||||
}
|
||||
|
||||
if v.list.Access(resource.NamespaceAccess) {
|
||||
v.namespaces = make(map[int]string, config.MaxFavoritesNS)
|
||||
for i, n := range config.Root.FavNamespaces() {
|
||||
aa[tcell.Key(numKeys[i])] = newKeyAction(n, v.switchNamespaceCmd, true)
|
||||
v.namespaces[i] = n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
aa[tcell.KeyCtrlR] = newKeyAction("Refresh", v.refreshCmd)
|
||||
aa[tcell.KeyCtrlR] = newKeyAction("Refresh", v.refreshCmd, false)
|
||||
aa[KeyHelp] = newKeyAction("Help", v.app.noopCmd, false)
|
||||
aa[KeyP] = newKeyAction("Previous", v.app.prevCmd, false)
|
||||
|
||||
if v.list.Access(resource.EditAccess) {
|
||||
aa[KeyE] = newKeyAction("Edit", v.editCmd)
|
||||
aa[KeyE] = newKeyAction("Edit", v.editCmd, true)
|
||||
}
|
||||
|
||||
if v.list.Access(resource.DeleteAccess) {
|
||||
aa[tcell.KeyCtrlD] = newKeyAction("Delete", v.deleteCmd)
|
||||
aa[tcell.KeyCtrlD] = newKeyAction("Delete", v.deleteCmd, true)
|
||||
}
|
||||
if v.list.Access(resource.ViewAccess) {
|
||||
aa[KeyV] = newKeyAction("View", v.viewCmd)
|
||||
aa[KeyV] = newKeyAction("View", v.viewCmd, true)
|
||||
}
|
||||
if v.list.Access(resource.DescribeAccess) {
|
||||
aa[KeyD] = newKeyAction("Describe", v.describeCmd)
|
||||
aa[KeyD] = newKeyAction("Describe", v.describeCmd, true)
|
||||
}
|
||||
|
||||
aa[KeyHelp] = newKeyAction("Help", v.app.noopCmd)
|
||||
|
||||
if v.extraActionsFn != nil {
|
||||
v.extraActionsFn(aa)
|
||||
}
|
||||
|
||||
t := v.getTV()
|
||||
t.setActions(aa)
|
||||
v.app.setHints(t.hints())
|
||||
|
|
|
|||
|
|
@ -32,13 +32,13 @@ var logo = []string{
|
|||
}
|
||||
|
||||
// Splash screen definition
|
||||
type Splash struct {
|
||||
type splashView struct {
|
||||
*tview.Flex
|
||||
}
|
||||
|
||||
// NewSplash instantiates a new splash screen with product and company info.
|
||||
func NewSplash(rev string) *Splash {
|
||||
v := Splash{tview.NewFlex()}
|
||||
func newSplash(rev string) *splashView {
|
||||
v := splashView{tview.NewFlex()}
|
||||
|
||||
logo := tview.NewTextView()
|
||||
{
|
||||
|
|
@ -62,11 +62,11 @@ func NewSplash(rev string) *Splash {
|
|||
return &v
|
||||
}
|
||||
|
||||
func (v *Splash) layoutLogo(t *tview.TextView) {
|
||||
func (v *splashView) layoutLogo(t *tview.TextView) {
|
||||
logo := strings.Join(logo, "\n[orange::b]")
|
||||
fmt.Fprintf(t, "%s[orange::b]%s\n", strings.Repeat("\n", 2), logo)
|
||||
}
|
||||
|
||||
func (v *Splash) layoutRev(t *tview.TextView, rev string) {
|
||||
func (v *splashView) layoutRev(t *tview.TextView, rev string) {
|
||||
fmt.Fprintf(t, "[white::b]Revision [red::b]%s", rev)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,14 +53,14 @@ func newTableView(app *appView, title string, sortFn resource.SortFn) *tableView
|
|||
v.SetInputCapture(v.keyboard)
|
||||
}
|
||||
|
||||
v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd)
|
||||
v.actions[tcell.KeyEnter] = newKeyAction("Search", v.filterCmd)
|
||||
v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd)
|
||||
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd)
|
||||
v.actions[KeyG] = newKeyAction("Top", app.puntCmd)
|
||||
v.actions[KeyShiftG] = newKeyAction("Bottom", app.puntCmd)
|
||||
v.actions[KeyB] = newKeyAction("Down", v.pageDownCmd)
|
||||
v.actions[KeyF] = newKeyAction("Up", v.pageUpCmd)
|
||||
v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd, false)
|
||||
v.actions[tcell.KeyEnter] = newKeyAction("Search", v.filterCmd, false)
|
||||
v.actions[tcell.KeyEscape] = newKeyAction("Reset Filter", v.resetCmd, false)
|
||||
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false)
|
||||
v.actions[KeyG] = newKeyAction("Top", app.puntCmd, false)
|
||||
v.actions[KeyShiftG] = newKeyAction("Bottom", app.puntCmd, false)
|
||||
v.actions[KeyB] = newKeyAction("Down", v.pageDownCmd, false)
|
||||
v.actions[KeyF] = newKeyAction("Up", v.pageUpCmd, false)
|
||||
|
||||
return &v
|
||||
}
|
||||
|
|
@ -77,6 +77,7 @@ func (v *tableView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
|||
v.cmdBuff.add(evt.Rune())
|
||||
v.clearSelection()
|
||||
v.doUpdate(v.filtered())
|
||||
v.setSelection()
|
||||
return nil
|
||||
}
|
||||
key = tcell.Key(evt.Rune())
|
||||
|
|
@ -89,6 +90,12 @@ func (v *tableView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
|||
return evt
|
||||
}
|
||||
|
||||
func (v *tableView) setSelection() {
|
||||
if v.GetRowCount() > 0 {
|
||||
v.Select(1, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *tableView) pageUpCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
v.PageUp()
|
||||
return nil
|
||||
|
|
@ -113,7 +120,9 @@ func (v *tableView) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
|
||||
func (v *tableView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
v.app.flash(flashInfo, "Filtering off...")
|
||||
if !v.cmdBuff.empty() {
|
||||
v.app.flash(flashInfo, "Clearing filter...")
|
||||
}
|
||||
v.cmdBuff.reset()
|
||||
v.refresh()
|
||||
return nil
|
||||
|
|
|
|||
Loading…
Reference in New Issue