checkpoint
parent
3efb3f2596
commit
dbbf68fab9
|
|
@ -74,7 +74,7 @@ func run(cmd *cobra.Command, args []string) {
|
|||
log.Error().Msg(string(debug.Stack()))
|
||||
printLogo(color.Red)
|
||||
fmt.Printf("%s", color.Colorize("Boom!! ", color.Red))
|
||||
fmt.Println(color.Colorize(fmt.Sprintf("%v.", err), color.White))
|
||||
fmt.Println(color.Colorize(fmt.Sprintf("%v.", err), color.LightGray))
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ func printVersion(short bool) {
|
|||
|
||||
func printTuple(fmat, section, value string, outputColor color.Paint) {
|
||||
if outputColor != -1 {
|
||||
fmt.Printf(fmat, color.Colorize(section+":", outputColor), color.Colorize(value, color.White))
|
||||
fmt.Printf(fmat, color.Colorize(section+":", outputColor), color.Colorize(value, color.LightGray))
|
||||
return
|
||||
}
|
||||
fmt.Printf(fmat, section, value)
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -30,7 +30,7 @@ replace (
|
|||
require (
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||
github.com/atotto/clipboard v0.1.2
|
||||
github.com/derailed/tview v0.3.7
|
||||
github.com/derailed/tview v0.3.8
|
||||
github.com/drone/envsubst v1.0.2 // indirect
|
||||
github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect
|
||||
github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f // indirect
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -128,6 +128,8 @@ github.com/deislabs/oras v0.7.0 h1:RnDoFd3tQYODMiUqxgQ8JxlrlWL0/VMKIKRD01MmNYk=
|
|||
github.com/deislabs/oras v0.7.0/go.mod h1:sqMKPG3tMyIX9xwXUBRLhZ24o+uT4y6jgBD2RzUTKDM=
|
||||
github.com/derailed/tview v0.3.7 h1:q0eYai9blR6wAWz/+lo2Knacl/Pnv9YSfI4aYme1aok=
|
||||
github.com/derailed/tview v0.3.7/go.mod h1:GJ3k/TIzEE+sj1L09/usk6HrkjsdadSsb03eHgPbcII=
|
||||
github.com/derailed/tview v0.3.8 h1:P5UmN8piZ8SbbvdPF2gnGd2dNaU6xLZtyYFCgrbrELQ=
|
||||
github.com/derailed/tview v0.3.8/go.mod h1:GJ3k/TIzEE+sj1L09/usk6HrkjsdadSsb03eHgPbcII=
|
||||
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/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||
|
|
@ -709,6 +711,8 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
|
||||
helm.sh/helm v1.2.1 h1:Jrn7kKQqQ/hnFWZEX+9pMFvYqFexkzrBnGqYBmIph7c=
|
||||
helm.sh/helm v2.16.3+incompatible h1:a7P7FSGTBdK6ZsAcWWZZQXPIdzkgybD8CWd/Dy+jwf4=
|
||||
helm.sh/helm/v3 v3.0.2 h1:BggvLisIMrAc+Is5oAHVrlVxgwOOrMN8nddfQbm5gKo=
|
||||
helm.sh/helm/v3 v3.0.2/go.mod h1:KBxE6XWO57XSNA1PA9CvVLYRY0zWqYQTad84bNXp1lw=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
|
|
|||
|
|
@ -143,7 +143,6 @@ func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) {
|
|||
}
|
||||
|
||||
// CheckConnectivity return true if api server is cool or false otherwise.
|
||||
// BOZO!! No super sure about this approach either??
|
||||
func (a *APIClient) CheckConnectivity() (status bool) {
|
||||
defer func() {
|
||||
if !status {
|
||||
|
|
|
|||
|
|
@ -4,20 +4,22 @@ import (
|
|||
"fmt"
|
||||
)
|
||||
|
||||
const ColorFmt = "\x1b[%dm%s\x1b[0m"
|
||||
|
||||
// Paint describes a terminal color.
|
||||
type Paint int
|
||||
|
||||
// Defines basic ANSI colors.
|
||||
const (
|
||||
Black Paint = iota + 30
|
||||
Red
|
||||
Green
|
||||
Yellow
|
||||
Blue
|
||||
Magenta
|
||||
Cyan
|
||||
White
|
||||
DarkGray = 90
|
||||
Black Paint = iota + 30 // 30
|
||||
Red // 31
|
||||
Green // 32
|
||||
Yellow // 33
|
||||
Blue // 34
|
||||
Magenta // 35
|
||||
Cyan // 36
|
||||
LightGray // 37
|
||||
DarkGray = 90
|
||||
|
||||
Bold = 1
|
||||
)
|
||||
|
|
@ -25,7 +27,7 @@ const (
|
|||
// Colorize returns an ASCII colored string based on given color.
|
||||
func Colorize(s string, c Paint) string {
|
||||
if c == 0 {
|
||||
c = White
|
||||
return s
|
||||
}
|
||||
return fmt.Sprintf("\x1b[%dm%s\x1b[0m", c, s)
|
||||
return fmt.Sprintf(ColorFmt, c, s)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,27 @@
|
|||
package color
|
||||
package color_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/color"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestColorize(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
s string
|
||||
c Paint
|
||||
c color.Paint
|
||||
e string
|
||||
}{
|
||||
"white": {"blee", White, "\x1b[37mblee\x1b[0m"},
|
||||
"black": {"blee", Black, "\x1b[30mblee\x1b[0m"},
|
||||
"default": {"blee", 0, "\x1b[37mblee\x1b[0m"},
|
||||
"white": {"blee", color.LightGray, "\x1b[37mblee\x1b[0m"},
|
||||
"black": {"blee", color.Black, "\x1b[30mblee\x1b[0m"},
|
||||
"default": {"blee", 0, "blee"},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, Colorize(u.s, u.c))
|
||||
assert.Equal(t, u.e, color.Colorize(u.s, u.c))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,18 @@ func NewAliases() *Aliases {
|
|||
}
|
||||
}
|
||||
|
||||
// Keys returns all aliases keys.
|
||||
func (a *Aliases) Keys() []string {
|
||||
a.mx.RLock()
|
||||
defer a.mx.RUnlock()
|
||||
|
||||
ss := make([]string, 0, len(a.Alias))
|
||||
for k := range a.Alias {
|
||||
ss = append(ss, k)
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
// ShortNames return all shortnames.
|
||||
func (a *Aliases) ShortNames() ShortNames {
|
||||
a.mx.RLock()
|
||||
|
|
@ -107,6 +119,13 @@ func (a *Aliases) LoadFileAliases(path string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (a *Aliases) declare(key string, aliases ...string) {
|
||||
a.Alias[key] = key
|
||||
for _, alias := range aliases {
|
||||
a.Alias[alias] = key
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Aliases) loadDefaultAliases() {
|
||||
a.mx.Lock()
|
||||
defer a.mx.Unlock()
|
||||
|
|
@ -120,49 +139,16 @@ func (a *Aliases) loadDefaultAliases() {
|
|||
a.Alias["rb"] = "rbac.authorization.k8s.io/v1/rolebindings"
|
||||
a.Alias["np"] = "networking.k8s.io/v1/networkpolicies"
|
||||
|
||||
const contexts = "contexts"
|
||||
{
|
||||
a.Alias["ctx"] = contexts
|
||||
a.Alias[contexts] = contexts
|
||||
a.Alias["context"] = contexts
|
||||
}
|
||||
const users = "users"
|
||||
{
|
||||
a.Alias["usr"] = users
|
||||
a.Alias[users] = users
|
||||
a.Alias["user"] = users
|
||||
}
|
||||
const groups = "groups"
|
||||
{
|
||||
a.Alias["grp"] = groups
|
||||
a.Alias["group"] = groups
|
||||
a.Alias[groups] = groups
|
||||
}
|
||||
const portFwds = "portforwards"
|
||||
{
|
||||
a.Alias["pf"] = portFwds
|
||||
a.Alias[portFwds] = portFwds
|
||||
a.Alias["portforward"] = portFwds
|
||||
}
|
||||
const benchmarks = "benchmarks"
|
||||
{
|
||||
a.Alias["be"] = benchmarks
|
||||
a.Alias["benchmark"] = benchmarks
|
||||
a.Alias[benchmarks] = benchmarks
|
||||
}
|
||||
const dumps = "screendumps"
|
||||
{
|
||||
a.Alias["sd"] = dumps
|
||||
a.Alias["screendump"] = dumps
|
||||
a.Alias[dumps] = dumps
|
||||
}
|
||||
const pulses = "pulses"
|
||||
{
|
||||
a.Alias["hz"] = pulses
|
||||
a.Alias["pu"] = pulses
|
||||
a.Alias["pulse"] = pulses
|
||||
a.Alias["pulses"] = pulses
|
||||
}
|
||||
a.declare("help", "h", "?")
|
||||
a.declare("quit", "q", "Q")
|
||||
a.declare("aliases", "alias", "a")
|
||||
a.declare("contexts", "context", "ctx")
|
||||
a.declare("users", "user", "usr")
|
||||
a.declare("groups", "group", "grp")
|
||||
a.declare("portforwards", "portforward", "pf")
|
||||
a.declare("benchmarks", "benchmark", "be")
|
||||
a.declare("screendumps", "screendump", "sd")
|
||||
a.declare("pulses", "pulse", "pu", "hz")
|
||||
}
|
||||
|
||||
// Save alias to disk.
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ func TestConfigLoad(t *testing.T) {
|
|||
|
||||
assert.Equal(t, 2, cfg.K9s.RefreshRate)
|
||||
assert.Equal(t, 2000, cfg.K9s.Logger.BufferSize)
|
||||
assert.Equal(t, 200, cfg.K9s.Logger.TailCount)
|
||||
assert.Equal(t, int64(200), cfg.K9s.Logger.TailCount)
|
||||
assert.Equal(t, "minikube", cfg.K9s.CurrentContext)
|
||||
assert.Equal(t, "minikube", cfg.K9s.CurrentCluster)
|
||||
assert.NotNil(t, cfg.K9s.Clusters)
|
||||
|
|
@ -266,6 +266,7 @@ var expectedConfig = `k9s:
|
|||
logger:
|
||||
tail: 500
|
||||
buffer: 800
|
||||
sinceSeconds: 300
|
||||
currentContext: blee
|
||||
currentCluster: blee
|
||||
fullScreenLogs: false
|
||||
|
|
@ -314,7 +315,8 @@ var resetConfig = `k9s:
|
|||
readOnly: false
|
||||
logger:
|
||||
tail: 200
|
||||
buffer: 2000
|
||||
buffer: 1000
|
||||
sinceSeconds: 300
|
||||
currentContext: blee
|
||||
currentCluster: blee
|
||||
fullScreenLogs: false
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ func TestK9sValidate(t *testing.T) {
|
|||
c.Validate(mc, mk)
|
||||
|
||||
assert.Equal(t, 2, c.RefreshRate)
|
||||
assert.Equal(t, 50, c.Logger.TailCount)
|
||||
assert.Equal(t, int64(100), c.Logger.TailCount)
|
||||
assert.Equal(t, 1_000, c.Logger.BufferSize)
|
||||
assert.Equal(t, "ctx1", c.CurrentContext)
|
||||
assert.Equal(t, "c1", c.CurrentCluster)
|
||||
|
|
@ -45,7 +45,7 @@ func TestK9sValidateBlank(t *testing.T) {
|
|||
c.Validate(mc, mk)
|
||||
|
||||
assert.Equal(t, 2, c.RefreshRate)
|
||||
assert.Equal(t, 50, c.Logger.TailCount)
|
||||
assert.Equal(t, int64(100), c.Logger.TailCount)
|
||||
assert.Equal(t, 1_000, c.Logger.BufferSize)
|
||||
assert.Equal(t, "ctx1", c.CurrentContext)
|
||||
assert.Equal(t, "c1", c.CurrentCluster)
|
||||
|
|
|
|||
|
|
@ -5,25 +5,29 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
// DefaultLoggerTailCount tracks log tail size.
|
||||
DefaultLoggerTailCount = 50
|
||||
// DefaultLoggerBufferSize tracks the buffer size.
|
||||
// DefaultLoggerTailCount tracks default log tail size.
|
||||
DefaultLoggerTailCount = 100
|
||||
// DefaultLoggerBufferSize tracks default view buffer size.
|
||||
DefaultLoggerBufferSize = 1_000
|
||||
// MaxLogThreshold sets the max value for log size.
|
||||
MaxLogThreshold = 5_000
|
||||
MaxLogThreshold = 1_000
|
||||
// DefaultSinceSeconds tracks default log age.
|
||||
DefaultSinceSeconds = 5 * 60 // 5mins
|
||||
)
|
||||
|
||||
// Logger tracks logger options
|
||||
type Logger struct {
|
||||
TailCount int `yaml:"tail"`
|
||||
BufferSize int `yaml:"buffer"`
|
||||
TailCount int64 `yaml:"tail"`
|
||||
BufferSize int `yaml:"buffer"`
|
||||
SinceSeconds int64 `yaml:"sinceSeconds"`
|
||||
}
|
||||
|
||||
// NewLogger returns a new instance.
|
||||
func NewLogger() *Logger {
|
||||
return &Logger{
|
||||
TailCount: DefaultLoggerTailCount,
|
||||
BufferSize: DefaultLoggerBufferSize,
|
||||
TailCount: DefaultLoggerTailCount,
|
||||
BufferSize: DefaultLoggerBufferSize,
|
||||
SinceSeconds: DefaultSinceSeconds,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,4 +45,7 @@ func (l *Logger) Validate(_ client.Connection, _ KubeSettings) {
|
|||
if l.BufferSize > MaxLogThreshold {
|
||||
l.BufferSize = MaxLogThreshold
|
||||
}
|
||||
if l.SinceSeconds == 0 {
|
||||
l.SinceSeconds = DefaultSinceSeconds
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ func TestNewLogger(t *testing.T) {
|
|||
l := config.NewLogger()
|
||||
l.Validate(nil, nil)
|
||||
|
||||
assert.Equal(t, 50, l.TailCount)
|
||||
assert.Equal(t, int64(100), l.TailCount)
|
||||
assert.Equal(t, 1_000, l.BufferSize)
|
||||
}
|
||||
|
||||
|
|
@ -19,6 +19,6 @@ func TestLoggerValidate(t *testing.T) {
|
|||
var l config.Logger
|
||||
l.Validate(nil, nil)
|
||||
|
||||
assert.Equal(t, 50, l.TailCount)
|
||||
assert.Equal(t, int64(100), l.TailCount)
|
||||
assert.Equal(t, 1_000, l.BufferSize)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,13 @@ type (
|
|||
|
||||
// Log tracks Log styles.
|
||||
Log struct {
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
Indicator LogIndicator `yaml:"indicator"`
|
||||
}
|
||||
|
||||
// LogIndicator tracks log view indicator.
|
||||
LogIndicator struct {
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
}
|
||||
|
|
@ -259,15 +266,21 @@ func newStatus() Status {
|
|||
}
|
||||
}
|
||||
|
||||
// NewLog returns a new log style.
|
||||
func newLog() Log {
|
||||
return Log{
|
||||
FgColor: "lightskyblue",
|
||||
FgColor: "lightskyblue",
|
||||
BgColor: "black",
|
||||
Indicator: newLogIndicator(),
|
||||
}
|
||||
}
|
||||
|
||||
func newLogIndicator() LogIndicator {
|
||||
return LogIndicator{
|
||||
FgColor: "dodgerblue",
|
||||
BgColor: "black",
|
||||
}
|
||||
}
|
||||
|
||||
// NewYaml returns a new yaml style.
|
||||
func newYaml() Yaml {
|
||||
return Yaml{
|
||||
KeyColor: "steelblue",
|
||||
|
|
@ -276,7 +289,6 @@ func newYaml() Yaml {
|
|||
}
|
||||
}
|
||||
|
||||
// NewTitle returns a new title style.
|
||||
func newTitle() Title {
|
||||
return Title{
|
||||
FgColor: "aqua",
|
||||
|
|
@ -287,7 +299,6 @@ func newTitle() Title {
|
|||
}
|
||||
}
|
||||
|
||||
// NewInfo returns a new info style.
|
||||
func newInfo() Info {
|
||||
return Info{
|
||||
SectionColor: "white",
|
||||
|
|
@ -295,7 +306,6 @@ func newInfo() Info {
|
|||
}
|
||||
}
|
||||
|
||||
// NewXray returns a new xray style.
|
||||
func newXray() Xray {
|
||||
return Xray{
|
||||
FgColor: "aqua",
|
||||
|
|
@ -306,7 +316,6 @@ func newXray() Xray {
|
|||
}
|
||||
}
|
||||
|
||||
// NewTable returns a new table style.
|
||||
func newTable() Table {
|
||||
return Table{
|
||||
FgColor: "aqua",
|
||||
|
|
@ -317,7 +326,6 @@ func newTable() Table {
|
|||
}
|
||||
}
|
||||
|
||||
// NewTableHeader returns a new table header style.
|
||||
func newTableHeader() TableHeader {
|
||||
return TableHeader{
|
||||
FgColor: "white",
|
||||
|
|
@ -326,7 +334,6 @@ func newTableHeader() TableHeader {
|
|||
}
|
||||
}
|
||||
|
||||
// NewCrumb returns a new crumbs style.
|
||||
func newCrumb() Crumb {
|
||||
return Crumb{
|
||||
FgColor: "black",
|
||||
|
|
@ -335,7 +342,6 @@ func newCrumb() Crumb {
|
|||
}
|
||||
}
|
||||
|
||||
// NewBorder returns a new border style.
|
||||
func newBorder() Border {
|
||||
return Border{
|
||||
FgColor: "dodgerblue",
|
||||
|
|
@ -343,7 +349,6 @@ func newBorder() Border {
|
|||
}
|
||||
}
|
||||
|
||||
// NewMenu returns a new menu style.
|
||||
func newMenu() Menu {
|
||||
return Menu{
|
||||
FgColor: "white",
|
||||
|
|
@ -464,6 +469,7 @@ func (s *Styles) Load(path string) error {
|
|||
func (s *Styles) Update() {
|
||||
tview.Styles.PrimitiveBackgroundColor = s.BgColor()
|
||||
tview.Styles.ContrastBackgroundColor = s.BgColor()
|
||||
tview.Styles.MoreContrastBackgroundColor = s.BgColor()
|
||||
tview.Styles.PrimaryTextColor = s.FgColor()
|
||||
tview.Styles.BorderColor = s.K9s.Frame.Border.FgColor.Color()
|
||||
tview.Styles.FocusColor = s.K9s.Frame.Border.FocusColor.Color()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package dao
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
|
|
@ -13,7 +12,6 @@ import (
|
|||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
||||
)
|
||||
|
||||
|
|
@ -60,37 +58,12 @@ func (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error
|
|||
}
|
||||
|
||||
// TailLogs tails a given container logs
|
||||
func (c *Container) TailLogs(ctx context.Context, logChan chan<- []byte, opts LogOptions) error {
|
||||
fac, ok := ctx.Value(internal.KeyFactory).(Factory)
|
||||
if !ok {
|
||||
return errors.New("Expecting an informer")
|
||||
}
|
||||
o, err := fac.Get("v1/pods", opts.Path, true, labels.Everything())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func (c *Container) TailLogs(ctx context.Context, logChan LogChan, opts LogOptions) error {
|
||||
log.Debug().Msgf("CONTAINER-LOGS")
|
||||
po := Pod{}
|
||||
po.Init(c.Factory, client.NewGVR("v1/pods"))
|
||||
|
||||
var po v1.Pod
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tailLogs(ctx, c, logChan, opts)
|
||||
}
|
||||
|
||||
// Logs fetch container logs for a given pod and container.
|
||||
func (c *Container) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error) {
|
||||
ns, _ := client.Namespaced(path)
|
||||
auth, err := c.Client().CanI(ns, "v1/pods:log", client.GetAccess)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !auth {
|
||||
return nil, fmt.Errorf("user is not authorized to view pod logs")
|
||||
}
|
||||
|
||||
ns, n := client.Namespaced(path)
|
||||
return c.Client().DialOrDie().CoreV1().Pods(ns).GetLogs(n, opts), nil
|
||||
return po.TailLogs(ctx, logChan, opts)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ func (d *Deployment) Restart(path string) error {
|
|||
}
|
||||
|
||||
// TailLogs tail logs for all pods represented by this Deployment.
|
||||
func (d *Deployment) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error {
|
||||
func (d *Deployment) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
|
||||
dp, err := d.Load(d.Factory, opts.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ func (d *DaemonSet) Restart(path string) error {
|
|||
}
|
||||
|
||||
// TailLogs tail logs for all pods represented by this DaemonSet.
|
||||
func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error {
|
||||
func (d *DaemonSet) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
|
||||
ds, err := d.GetInstance(opts.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -74,7 +74,7 @@ func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptio
|
|||
return podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts)
|
||||
}
|
||||
|
||||
func podLogs(ctx context.Context, c chan<- []byte, sel map[string]string, opts LogOptions) error {
|
||||
func podLogs(ctx context.Context, c LogChan, sel map[string]string, opts LogOptions) error {
|
||||
f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
|
||||
if !ok {
|
||||
return errors.New("expecting a context factory")
|
||||
|
|
@ -89,14 +89,11 @@ func podLogs(ctx context.Context, c chan<- []byte, sel map[string]string, opts L
|
|||
}
|
||||
|
||||
ns, _ := client.Namespaced(opts.Path)
|
||||
oo, err := f.List("v1/pods", ns, false, lsel)
|
||||
oo, err := f.List("v1/pods", ns, true, lsel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(oo) > 1 {
|
||||
opts.MultiPods = true
|
||||
}
|
||||
opts.MultiPods = true
|
||||
|
||||
po := Pod{}
|
||||
po.Init(f, client.NewGVR("v1/pods"))
|
||||
|
|
|
|||
|
|
@ -12,6 +12,14 @@ import (
|
|||
"k8s.io/cli-runtime/pkg/printers"
|
||||
)
|
||||
|
||||
// IsFuzzySelector checks if filter is fuzzy or not.
|
||||
func IsFuzzySelector(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
return fuzzyRx.MatchString(s)
|
||||
}
|
||||
|
||||
func toPerc(v1, v2 float64) float64 {
|
||||
if v2 == 0 {
|
||||
return 0
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ type Job struct {
|
|||
}
|
||||
|
||||
// TailLogs tail logs for all pods represented by this Job.
|
||||
func (j *Job) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error {
|
||||
func (j *Job) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
|
||||
o, err := j.Factory.Get(j.gvr.String(), opts.Path, true, labels.Everything())
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sahilm/fuzzy"
|
||||
)
|
||||
|
||||
// LogChan represents a channel for logs.
|
||||
type LogChan chan *LogItem
|
||||
|
||||
// LogItem represents a container log line.
|
||||
type LogItem struct {
|
||||
Pod, Container, Timestamp string
|
||||
Bytes []byte
|
||||
}
|
||||
|
||||
// NewLogItem returns a new item.
|
||||
func NewLogItem(b []byte) *LogItem {
|
||||
space := []byte(" ")
|
||||
var l LogItem
|
||||
|
||||
cols := bytes.Split(b[:len(b)-1], space)
|
||||
l.Timestamp = string(cols[0])
|
||||
l.Bytes = bytes.Join(cols[1:], space)
|
||||
|
||||
return &l
|
||||
}
|
||||
|
||||
// NewLogItemFromString returns a new item.
|
||||
func NewLogItemFromString(s string) *LogItem {
|
||||
l := LogItem{Bytes: []byte(s)}
|
||||
l.Timestamp = time.Now().String()
|
||||
|
||||
return &l
|
||||
}
|
||||
|
||||
// IsEmpty checks if the entry is empty.
|
||||
func (l *LogItem) IsEmpty() bool {
|
||||
return len(l.Bytes) == 0
|
||||
}
|
||||
|
||||
// Render returns a log line as string.
|
||||
func (l *LogItem) Render(showTime bool) []byte {
|
||||
bb := make([]byte, 0, 100+len(l.Bytes))
|
||||
if showTime {
|
||||
bb = append(bb, fmt.Sprintf("%-30s ", l.Timestamp)...)
|
||||
}
|
||||
|
||||
if l.Pod != "" {
|
||||
bb = append(bb, l.Pod...)
|
||||
bb = append(bb, ':')
|
||||
bb = append(bb, l.Container...)
|
||||
bb = append(bb, ' ')
|
||||
} else if l.Container != "" {
|
||||
bb = append(bb, l.Container...)
|
||||
bb = append(bb, ' ')
|
||||
}
|
||||
bb = append(bb, l.Bytes...)
|
||||
|
||||
return bb
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// LogItems represents a collection of log items.
|
||||
type LogItems []*LogItem
|
||||
|
||||
// Lines returns a collection of log lines.
|
||||
func (l LogItems) Lines() []string {
|
||||
ll := make([]string, len(l))
|
||||
for i, item := range l {
|
||||
ll[i] = string(item.Render(false))
|
||||
}
|
||||
|
||||
return ll
|
||||
}
|
||||
|
||||
// Render returns logs as a collection of strings.
|
||||
func (l LogItems) Render(showTime bool, ll [][]byte) {
|
||||
for i, item := range l {
|
||||
ll[i] = item.Render(showTime)
|
||||
}
|
||||
}
|
||||
|
||||
// DumpDebug for debuging
|
||||
func (l LogItems) DumpDebug(m string) {
|
||||
fmt.Println(m + strings.Repeat("-", 50))
|
||||
for i, line := range l {
|
||||
fmt.Println(i, string(line.Bytes))
|
||||
}
|
||||
}
|
||||
|
||||
// Filter filters out log items based on given filter.
|
||||
func (l LogItems) Filter(q string) ([]int, error) {
|
||||
if q == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if IsFuzzySelector(q) {
|
||||
return l.fuzzyFilter(strings.TrimSpace(q[2:])), nil
|
||||
}
|
||||
indexes, err := l.filterLogs(q)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Logs filter failed")
|
||||
return nil, err
|
||||
}
|
||||
return indexes, nil
|
||||
}
|
||||
|
||||
var fuzzyRx = regexp.MustCompile(`\A\-f`)
|
||||
|
||||
func (l LogItems) fuzzyFilter(q string) []int {
|
||||
q = strings.TrimSpace(q)
|
||||
matches := make([]int, 0, len(l))
|
||||
mm := fuzzy.Find(q, l.Lines())
|
||||
for _, m := range mm {
|
||||
matches = append(matches, m.Index)
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
func (l LogItems) filterLogs(q string) ([]int, error) {
|
||||
rx, err := regexp.Compile(`(?i)` + q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matches := make([]int, 0, len(l))
|
||||
for i, line := range l.Lines() {
|
||||
if rx.MatchString(line) {
|
||||
matches = append(matches, i)
|
||||
}
|
||||
}
|
||||
|
||||
return matches, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
package dao_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zerolog.SetGlobalLevel(zerolog.FatalLevel)
|
||||
}
|
||||
|
||||
func TestLogItemsFilter(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
q string
|
||||
opts dao.LogOptions
|
||||
e []int
|
||||
err error
|
||||
}{
|
||||
"empty": {
|
||||
opts: dao.LogOptions{},
|
||||
},
|
||||
"pod-name": {
|
||||
q: "blee",
|
||||
opts: dao.LogOptions{
|
||||
Path: "fred/blee",
|
||||
Container: "c1",
|
||||
},
|
||||
e: []int{0, 1, 2},
|
||||
},
|
||||
"container-name": {
|
||||
q: "c1",
|
||||
opts: dao.LogOptions{
|
||||
Path: "fred/blee",
|
||||
Container: "c1",
|
||||
},
|
||||
e: []int{0, 1, 2},
|
||||
},
|
||||
"message": {
|
||||
q: "zorg",
|
||||
opts: dao.LogOptions{
|
||||
Path: "fred/blee",
|
||||
Container: "c1",
|
||||
},
|
||||
e: []int{2},
|
||||
},
|
||||
"fuzzy": {
|
||||
q: "-f zorg",
|
||||
opts: dao.LogOptions{
|
||||
Path: "fred/blee",
|
||||
Container: "c1",
|
||||
},
|
||||
e: []int{2},
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
ii := dao.LogItems{
|
||||
dao.NewLogItem([]byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3..."))),
|
||||
dao.NewLogItemFromString("Bumble bee tuna"),
|
||||
dao.NewLogItemFromString("Jean Batiste Emmanuel Zorg"),
|
||||
}
|
||||
t.Run(k, func(t *testing.T) {
|
||||
_, n := client.Namespaced(u.opts.Path)
|
||||
for _, i := range ii {
|
||||
i.Pod, i.Container = n, u.opts.Container
|
||||
}
|
||||
res, err := ii.Filter(u.q)
|
||||
assert.Equal(t, u.err, err)
|
||||
if err == nil {
|
||||
assert.Equal(t, u.e, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogItemsRender(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
opts dao.LogOptions
|
||||
e string
|
||||
}{
|
||||
"empty": {
|
||||
opts: dao.LogOptions{},
|
||||
e: "Testing 1,2,3...",
|
||||
},
|
||||
"container": {
|
||||
opts: dao.LogOptions{
|
||||
Container: "fred",
|
||||
},
|
||||
e: "fred Testing 1,2,3...",
|
||||
},
|
||||
"pod": {
|
||||
opts: dao.LogOptions{
|
||||
Path: "blee/fred",
|
||||
Container: "blee",
|
||||
},
|
||||
e: "fred:blee Testing 1,2,3...",
|
||||
},
|
||||
"full": {
|
||||
opts: dao.LogOptions{
|
||||
Path: "blee/fred",
|
||||
Container: "blee",
|
||||
ShowTimestamp: true,
|
||||
},
|
||||
e: "2018-12-14T10:36:43.326972-07:00 fred:blee Testing 1,2,3...",
|
||||
},
|
||||
}
|
||||
|
||||
s := []byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3..."))
|
||||
ii := dao.LogItems{dao.NewLogItem(s)}
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
_, n := client.Namespaced(u.opts.Path)
|
||||
ii[0].Pod, ii[0].Container = n, u.opts.Container
|
||||
t.Run(k, func(t *testing.T) {
|
||||
res := make([][]byte, 1)
|
||||
ii.Render(u.opts.ShowTimestamp, res)
|
||||
assert.Equal(t, u.e, string(res[0]))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogItemEmpty(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
s string
|
||||
e bool
|
||||
}{
|
||||
"empty": {s: "", e: true},
|
||||
"full": {s: "Testing 1,2,3..."},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
i := dao.NewLogItemFromString(u.s)
|
||||
assert.Equal(t, u.e, i.IsEmpty())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogItemRender(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
opts dao.LogOptions
|
||||
e string
|
||||
}{
|
||||
"empty": {
|
||||
opts: dao.LogOptions{},
|
||||
e: "Testing 1,2,3...",
|
||||
},
|
||||
"container": {
|
||||
opts: dao.LogOptions{
|
||||
Container: "fred",
|
||||
},
|
||||
e: "fred Testing 1,2,3...",
|
||||
},
|
||||
"pod": {
|
||||
opts: dao.LogOptions{
|
||||
Path: "blee/fred",
|
||||
Container: "blee",
|
||||
},
|
||||
e: "fred:blee Testing 1,2,3...",
|
||||
},
|
||||
"full": {
|
||||
opts: dao.LogOptions{
|
||||
Path: "blee/fred",
|
||||
Container: "blee",
|
||||
ShowTimestamp: true,
|
||||
},
|
||||
e: "2018-12-14T10:36:43.326972-07:00 fred:blee Testing 1,2,3...",
|
||||
},
|
||||
}
|
||||
|
||||
s := []byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3..."))
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
i := dao.NewLogItem(s)
|
||||
_, n := client.Namespaced(u.opts.Path)
|
||||
i.Pod, i.Container = n, u.opts.Container
|
||||
|
||||
assert.Equal(t, u.e, string(i.Render(u.opts.ShowTimestamp)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLogItemRender(b *testing.B) {
|
||||
s := []byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3..."))
|
||||
i := dao.NewLogItem(s)
|
||||
i.Pod, i.Container = "fred", "blee"
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for n := 0; n < b.N; n++ {
|
||||
i.Render(true)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,11 @@ package dao
|
|||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/color"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// LogOptions represent logger options.
|
||||
|
|
@ -12,11 +14,13 @@ type LogOptions struct {
|
|||
Path string
|
||||
Container string
|
||||
Lines int64
|
||||
Color color.Paint
|
||||
Previous bool
|
||||
SingleContainer bool
|
||||
MultiPods bool
|
||||
ShowTimestamp bool
|
||||
SinceTime string
|
||||
SinceSeconds int64
|
||||
In, Out string
|
||||
}
|
||||
|
||||
// HasContainer checks if a container is present.
|
||||
|
|
@ -24,6 +28,33 @@ func (o LogOptions) HasContainer() bool {
|
|||
return o.Container != ""
|
||||
}
|
||||
|
||||
// ToPodLogOptions returns pod log options.
|
||||
func (o LogOptions) ToPodLogOptions() *v1.PodLogOptions {
|
||||
opts := v1.PodLogOptions{
|
||||
Follow: true,
|
||||
Timestamps: true,
|
||||
Container: o.Container,
|
||||
Previous: o.Previous,
|
||||
TailLines: &o.Lines,
|
||||
}
|
||||
|
||||
if o.SinceSeconds < 0 {
|
||||
return &opts
|
||||
}
|
||||
if o.SinceSeconds != 0 {
|
||||
opts.SinceSeconds = &o.SinceSeconds
|
||||
return &opts
|
||||
}
|
||||
if o.SinceTime == "" {
|
||||
return &opts
|
||||
}
|
||||
if t, err := time.Parse(time.RFC3339, o.SinceTime); err == nil {
|
||||
opts.SinceTime = &metav1.Time{Time: t.Add(time.Second)}
|
||||
}
|
||||
|
||||
return &opts
|
||||
}
|
||||
|
||||
// FixedSizeName returns a normalize fixed size pod name if possible.
|
||||
func (o LogOptions) FixedSizeName() string {
|
||||
_, n := client.Namespaced(o.Path)
|
||||
|
|
@ -39,35 +70,21 @@ func (o LogOptions) FixedSizeName() string {
|
|||
return Truncate(strings.Join(s, "-"), 15) + "-" + tokens[len(tokens)-1]
|
||||
}
|
||||
|
||||
func colorize(c color.Paint, txt string) string {
|
||||
if c == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return color.Colorize(txt, c)
|
||||
}
|
||||
|
||||
// DecorateLog add a log header to display po/co information along with the log message.
|
||||
func (o LogOptions) DecorateLog(bytes []byte) []byte {
|
||||
func (o LogOptions) DecorateLog(bytes []byte) *LogItem {
|
||||
item := NewLogItem(bytes)
|
||||
if len(bytes) == 0 {
|
||||
return bytes
|
||||
return item
|
||||
}
|
||||
|
||||
bytes = bytes[:len(bytes)-1]
|
||||
_, n := client.Namespaced(o.Path)
|
||||
|
||||
var prefix []byte
|
||||
if o.MultiPods {
|
||||
prefix = []byte(colorize(o.Color, n+":"+o.Container+" "))
|
||||
_, pod := client.Namespaced(o.Path)
|
||||
item.Pod, item.Container = pod, o.Container
|
||||
}
|
||||
|
||||
if !o.SingleContainer {
|
||||
prefix = []byte(colorize(o.Color, o.Container+" "))
|
||||
item.Container = o.Container
|
||||
}
|
||||
|
||||
if len(prefix) == 0 {
|
||||
return bytes
|
||||
}
|
||||
|
||||
return append(prefix, bytes...)
|
||||
return item
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import (
|
|||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/color"
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
"github.com/derailed/k9s/internal/watch"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
|
@ -171,14 +170,8 @@ func (p *Pod) GetInstance(fqn string) (*v1.Pod, error) {
|
|||
}
|
||||
|
||||
// TailLogs tails a given container logs
|
||||
func (p *Pod) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error {
|
||||
if !opts.HasContainer() {
|
||||
return p.logs(ctx, c, opts)
|
||||
}
|
||||
return tailLogs(ctx, p, c, opts)
|
||||
}
|
||||
|
||||
func (p *Pod) logs(ctx context.Context, c chan<- []byte, opts LogOptions) error {
|
||||
func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
|
||||
log.Debug().Msgf("TAIL-LOGS for %q:%q", opts.Path, opts.Container)
|
||||
fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
|
||||
if !ok {
|
||||
return errors.New("Expecting an informer")
|
||||
|
|
@ -192,41 +185,51 @@ func (p *Pod) logs(ctx context.Context, c chan<- []byte, opts LogOptions) error
|
|||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Color = asColor(po.Name)
|
||||
|
||||
rcos := loggableContainers(po.Status)
|
||||
if opts.HasContainer() {
|
||||
opts.SingleContainer = true
|
||||
if !in(rcos, opts.Container) {
|
||||
return fmt.Errorf("no logs found for container %s on %s", opts.Container, opts.Path)
|
||||
}
|
||||
if err := tailLogs(ctx, p, c, opts); err != nil {
|
||||
log.Error().Err(err).Msgf("Getting logs for %s failed", opts.Container)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(po.Spec.InitContainers)+len(po.Spec.Containers) == 1 {
|
||||
opts.SingleContainer = true
|
||||
}
|
||||
|
||||
var tailed bool
|
||||
for _, co := range po.Spec.InitContainers {
|
||||
opts.Container = co.Name
|
||||
if err := p.TailLogs(ctx, c, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
tailed = true
|
||||
}
|
||||
rcos := loggableContainers(po.Status)
|
||||
for _, co := range po.Spec.Containers {
|
||||
if in(rcos, co.Name) {
|
||||
opts.Container = co.Name
|
||||
if err := p.TailLogs(ctx, c, opts); err != nil {
|
||||
if err := tailLogs(ctx, p, c, opts); err != nil {
|
||||
log.Error().Err(err).Msgf("Getting logs for %s failed", co.Name)
|
||||
return err
|
||||
}
|
||||
tailed = true
|
||||
}
|
||||
}
|
||||
|
||||
if !tailed {
|
||||
return fmt.Errorf("no loggable containers found for pod %s", opts.Path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tailLogs(ctx context.Context, logger Logger, c chan<- []byte, opts LogOptions) error {
|
||||
log.Debug().Msgf("Tailing logs for %q -- %q", opts.Path, opts.Container)
|
||||
o := v1.PodLogOptions{
|
||||
Follow: true,
|
||||
Timestamps: false,
|
||||
Container: opts.Container,
|
||||
Previous: opts.Previous,
|
||||
TailLines: &opts.Lines,
|
||||
}
|
||||
req, err := logger.Logs(opts.Path, &o)
|
||||
func tailLogs(ctx context.Context, logger Logger, c LogChan, opts LogOptions) error {
|
||||
log.Debug().Msgf("Tailing logs for %q:%q", opts.Path, opts.Container)
|
||||
req, err := logger.Logs(opts.Path, opts.ToPodLogOptions())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -236,15 +239,15 @@ func tailLogs(ctx context.Context, logger Logger, c chan<- []byte, opts LogOptio
|
|||
stream, err := req.Stream()
|
||||
if err != nil {
|
||||
c <- opts.DecorateLog([]byte(err.Error() + "\n"))
|
||||
log.Error().Err(err).Msgf("Log stream failed for `%s", opts.Path)
|
||||
return fmt.Errorf("Unable to obtain log stream for %s", opts.Path)
|
||||
log.Error().Err(err).Msgf("Unable to obtain log stream failed for `%s", opts.Path)
|
||||
return err
|
||||
}
|
||||
go readLogs(stream, c, opts)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readLogs(stream io.ReadCloser, c chan<- []byte, opts LogOptions) {
|
||||
func readLogs(stream io.ReadCloser, c LogChan, opts LogOptions) {
|
||||
defer func() {
|
||||
log.Debug().Msgf(">>> Closing stream `%s", opts.Path)
|
||||
if err := stream.Close(); err != nil {
|
||||
|
|
@ -258,11 +261,12 @@ func readLogs(stream io.ReadCloser, c chan<- []byte, opts LogOptions) {
|
|||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Read error")
|
||||
if err == io.EOF {
|
||||
c <- opts.DecorateLog([]byte("<STREAM> closed\n"))
|
||||
log.Warn().Err(err).Msgf("stream closed")
|
||||
c <- NewLogItemFromString("<STREAM> closed")
|
||||
return
|
||||
}
|
||||
log.Error().Err(err).Msgf("stream reader failed")
|
||||
c <- opts.DecorateLog([]byte("<STREAM> failed\n"))
|
||||
c <- NewLogItemFromString("<STREAM> failed")
|
||||
return
|
||||
}
|
||||
c <- opts.DecorateLog(bytes)
|
||||
|
|
@ -331,19 +335,13 @@ func extractFQN(o runtime.Object) string {
|
|||
func loggableContainers(s v1.PodStatus) []string {
|
||||
var rcos []string
|
||||
for _, c := range s.ContainerStatuses {
|
||||
rcos = append(rcos, c.Name)
|
||||
if c.State.Waiting == nil {
|
||||
rcos = append(rcos, c.Name)
|
||||
}
|
||||
}
|
||||
return rcos
|
||||
}
|
||||
|
||||
func asColor(n string) color.Paint {
|
||||
var sum int
|
||||
for _, r := range n {
|
||||
sum += int(r)
|
||||
}
|
||||
return color.Paint(30 + 2 + sum%6)
|
||||
}
|
||||
|
||||
// Check if string is in a string list.
|
||||
func in(ll []string, s string) bool {
|
||||
for _, l := range ll {
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ func (s *StatefulSet) Restart(path string) error {
|
|||
}
|
||||
|
||||
// TailLogs tail logs for all pods represented by this StatefulSet.
|
||||
func (s *StatefulSet) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error {
|
||||
func (s *StatefulSet) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
|
||||
sts, err := s.getStatefulSet(opts.Path)
|
||||
if err != nil {
|
||||
return errors.New("expecting StatefulSet resource")
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ type Service struct {
|
|||
}
|
||||
|
||||
// TailLogs tail logs for all pods represented by this Service.
|
||||
func (s *Service) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error {
|
||||
func (s *Service) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
|
||||
svc, err := s.GetInstance(opts.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ type NodeMaintainer interface {
|
|||
// Loggable represents resources with logs.
|
||||
type Loggable interface {
|
||||
// TaiLogs streams resource logs.
|
||||
TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error
|
||||
TailLogs(ctx context.Context, c LogChan, opts LogOptions) error
|
||||
}
|
||||
|
||||
// Describer describes a resource.
|
||||
|
|
|
|||
|
|
@ -1,37 +1,35 @@
|
|||
package ui
|
||||
package model
|
||||
|
||||
const maxBuff = 10
|
||||
|
||||
const (
|
||||
// CommandBuff indicates a command buffer.
|
||||
CommandBuff BufferKind = 1 << iota
|
||||
// FilterBuff indicates a search buffer.
|
||||
FilterBuff
|
||||
// Command represents a command buffer.
|
||||
Command BufferKind = 1 << iota
|
||||
// Filter represents a filter buffer.
|
||||
Filter
|
||||
)
|
||||
|
||||
type (
|
||||
// BufferKind indicates a buffer type
|
||||
BufferKind int8
|
||||
// BufferKind indicates a buffer type
|
||||
type BufferKind int8
|
||||
|
||||
// BuffWatcher represents a command buffer listener.
|
||||
BuffWatcher interface {
|
||||
// Changed indicates the buffer was changed.
|
||||
BufferChanged(s string)
|
||||
// BuffWatcher represents a command buffer listener.
|
||||
type BuffWatcher interface {
|
||||
// Changed indicates the buffer was changed.
|
||||
BufferChanged(s string)
|
||||
|
||||
// Active indicates the buff activity changed.
|
||||
BufferActive(state bool, kind BufferKind)
|
||||
}
|
||||
// Active indicates the buff activity changed.
|
||||
BufferActive(state bool, kind BufferKind)
|
||||
}
|
||||
|
||||
// CmdBuff represents user command input.
|
||||
CmdBuff struct {
|
||||
buff []rune
|
||||
listeners []BuffWatcher
|
||||
hotKey rune
|
||||
kind BufferKind
|
||||
sticky bool
|
||||
active bool
|
||||
}
|
||||
)
|
||||
// CmdBuff represents user command input.
|
||||
type CmdBuff struct {
|
||||
buff []rune
|
||||
listeners []BuffWatcher
|
||||
hotKey rune
|
||||
kind BufferKind
|
||||
sticky bool
|
||||
active bool
|
||||
}
|
||||
|
||||
// NewCmdBuff returns a new command buffer.
|
||||
func NewCmdBuff(key rune, kind BufferKind) *CmdBuff {
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
package ui_test
|
||||
package model_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ func (l *testListener) BufferChanged(s string) {
|
|||
l.text = s
|
||||
}
|
||||
|
||||
func (l *testListener) BufferActive(s bool, _ ui.BufferKind) {
|
||||
func (l *testListener) BufferActive(s bool, _ model.BufferKind) {
|
||||
if s {
|
||||
l.act++
|
||||
return
|
||||
|
|
@ -26,7 +26,7 @@ func (l *testListener) BufferActive(s bool, _ ui.BufferKind) {
|
|||
}
|
||||
|
||||
func TestCmdBuffActivate(t *testing.T) {
|
||||
b, l := ui.NewCmdBuff('>', ui.CommandBuff), testListener{}
|
||||
b, l := model.NewCmdBuff('>', model.Command), testListener{}
|
||||
b.AddListener(&l)
|
||||
|
||||
b.SetActive(true)
|
||||
|
|
@ -36,7 +36,7 @@ func TestCmdBuffActivate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCmdBuffDeactivate(t *testing.T) {
|
||||
b, l := ui.NewCmdBuff('>', ui.CommandBuff), testListener{}
|
||||
b, l := model.NewCmdBuff('>', model.Command), testListener{}
|
||||
b.AddListener(&l)
|
||||
|
||||
b.SetActive(false)
|
||||
|
|
@ -46,7 +46,7 @@ func TestCmdBuffDeactivate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCmdBuffChanged(t *testing.T) {
|
||||
b, l := ui.NewCmdBuff('>', ui.CommandBuff), testListener{}
|
||||
b, l := model.NewCmdBuff('>', model.Command), testListener{}
|
||||
b.AddListener(&l)
|
||||
|
||||
b.Add('b')
|
||||
|
|
@ -78,7 +78,7 @@ func TestCmdBuffChanged(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCmdBuffAdd(t *testing.T) {
|
||||
b := ui.NewCmdBuff('>', ui.CommandBuff)
|
||||
b := model.NewCmdBuff('>', model.Command)
|
||||
|
||||
uu := []struct {
|
||||
runes []rune
|
||||
|
|
@ -99,7 +99,7 @@ func TestCmdBuffAdd(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCmdBuffDel(t *testing.T) {
|
||||
b := ui.NewCmdBuff('>', ui.CommandBuff)
|
||||
b := model.NewCmdBuff('>', model.Command)
|
||||
|
||||
uu := []struct {
|
||||
runes []rune
|
||||
|
|
@ -121,7 +121,7 @@ func TestCmdBuffDel(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCmdBuffEmpty(t *testing.T) {
|
||||
b := ui.NewCmdBuff('>', ui.CommandBuff)
|
||||
b := model.NewCmdBuff('>', model.Command)
|
||||
|
||||
uu := []struct {
|
||||
runes []rune
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
// SuggestionListener listens for suggestions.
|
||||
type SuggestionListener interface {
|
||||
BuffWatcher
|
||||
|
||||
// SuggestionChanged notifies suggestion changes.
|
||||
SuggestionChanged([]string)
|
||||
}
|
||||
|
||||
// SuggestionFunc produces suggestions.
|
||||
type SuggestionFunc func(s string) sort.StringSlice
|
||||
|
||||
// FishBuff represents a suggestion buffer.
|
||||
type FishBuff struct {
|
||||
*CmdBuff
|
||||
|
||||
suggestionFn SuggestionFunc
|
||||
}
|
||||
|
||||
// NewFishBuffer returns a new command buffer.
|
||||
func NewFishBuff(key rune, kind BufferKind) *FishBuff {
|
||||
return &FishBuff{CmdBuff: NewCmdBuff(key, kind)}
|
||||
}
|
||||
|
||||
// SetSuggestionFn sets up suggestions.
|
||||
func (f *FishBuff) SetSuggestionFn(fn SuggestionFunc) {
|
||||
f.suggestionFn = fn
|
||||
}
|
||||
|
||||
// Delete removes the last character from the buffer.
|
||||
func (f *FishBuff) Delete() {
|
||||
f.CmdBuff.Delete()
|
||||
if f.suggestionFn == nil {
|
||||
return
|
||||
}
|
||||
cc := f.suggestionFn(string(f.buff))
|
||||
f.fireSuggest(cc)
|
||||
}
|
||||
|
||||
// Add adds a new charater to the buffer.
|
||||
func (f *FishBuff) Add(r rune) {
|
||||
f.CmdBuff.Add(r)
|
||||
if f.suggestionFn == nil {
|
||||
return
|
||||
}
|
||||
cc := f.suggestionFn(string(f.buff))
|
||||
f.fireSuggest(cc)
|
||||
}
|
||||
|
||||
func (f *FishBuff) fireSuggest(cc []string) {
|
||||
for _, l := range f.listeners {
|
||||
if s, ok := l.(SuggestionListener); ok {
|
||||
s.SuggestionChanged(cc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,6 @@ package model
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -13,13 +11,12 @@ import (
|
|||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sahilm/fuzzy"
|
||||
)
|
||||
|
||||
// LogsListener represents a log model listener.
|
||||
type LogsListener interface {
|
||||
// LogChanged notifies the model changed.
|
||||
LogChanged([]string)
|
||||
LogChanged(dao.LogItems)
|
||||
|
||||
// LogCleanred indicates logs are cleared.
|
||||
LogCleared()
|
||||
|
|
@ -30,18 +27,17 @@ type LogsListener interface {
|
|||
|
||||
// Log represents a resource logger.
|
||||
type Log struct {
|
||||
factory dao.Factory
|
||||
lines []string
|
||||
listeners []LogsListener
|
||||
gvr client.GVR
|
||||
logOptions dao.LogOptions
|
||||
cancelFn context.CancelFunc
|
||||
mx sync.RWMutex
|
||||
filter string
|
||||
bufferSize int
|
||||
lastSent int
|
||||
showTimestamp bool
|
||||
flushTimeout time.Duration
|
||||
factory dao.Factory
|
||||
lines dao.LogItems
|
||||
listeners []LogsListener
|
||||
gvr client.GVR
|
||||
logOptions dao.LogOptions
|
||||
cancelFn context.CancelFunc
|
||||
mx sync.RWMutex
|
||||
filter string
|
||||
bufferSize int
|
||||
lastSent int
|
||||
flushTimeout time.Duration
|
||||
}
|
||||
|
||||
// NewLog returns a new model.
|
||||
|
|
@ -54,9 +50,28 @@ func NewLog(gvr client.GVR, opts dao.LogOptions, flushTimeout time.Duration) *Lo
|
|||
}
|
||||
}
|
||||
|
||||
// LogOptions returns the current log options.
|
||||
func (l *Log) LogOptions() dao.LogOptions {
|
||||
return l.logOptions
|
||||
}
|
||||
|
||||
// SinceSeconds returns since seconds option.
|
||||
func (l *Log) SinceSeconds() int64 {
|
||||
l.mx.RLock()
|
||||
defer l.mx.RUnlock()
|
||||
return l.logOptions.SinceSeconds
|
||||
}
|
||||
|
||||
func (l *Log) SetLogOptions(opts dao.LogOptions) {
|
||||
l.logOptions = opts
|
||||
l.Restart()
|
||||
}
|
||||
|
||||
// Configure sets logger configuration.
|
||||
func (l *Log) Configure(opts *config.Logger) {
|
||||
l.bufferSize, l.logOptions.Lines = opts.BufferSize, int64(opts.TailCount)
|
||||
l.bufferSize = opts.BufferSize
|
||||
l.logOptions.Lines = int64(opts.TailCount)
|
||||
l.logOptions.SinceSeconds = opts.SinceSeconds
|
||||
}
|
||||
|
||||
// GetPath returns resource path.
|
||||
|
|
@ -74,22 +89,25 @@ func (l *Log) Init(f dao.Factory) {
|
|||
func (l *Log) Clear() {
|
||||
l.mx.Lock()
|
||||
{
|
||||
l.lines, l.lastSent = []string{}, 0
|
||||
l.lines, l.lastSent = dao.LogItems{}, 0
|
||||
}
|
||||
l.mx.Unlock()
|
||||
l.fireLogCleared()
|
||||
}
|
||||
|
||||
// ShowTimestamp toggles timestamp on logs.
|
||||
func (l *Log) ShowTimestamp(b bool) {
|
||||
l.mx.RLock()
|
||||
defer l.mx.RUnlock()
|
||||
|
||||
l.showTimestamp = b
|
||||
// Refresh refreshes the logs.
|
||||
func (l *Log) Refresh() {
|
||||
l.fireLogCleared()
|
||||
l.fireLogChanged(l.lines)
|
||||
}
|
||||
|
||||
// Restart restarts the logger.
|
||||
func (l *Log) Restart() {
|
||||
l.Clear()
|
||||
l.Stop()
|
||||
l.Start()
|
||||
}
|
||||
|
||||
// Start initialize log tailer.
|
||||
func (l *Log) Start() {
|
||||
if err := l.load(); err != nil {
|
||||
|
|
@ -108,21 +126,21 @@ func (l *Log) Stop() {
|
|||
}
|
||||
|
||||
// Set sets the log lines (for testing only!)
|
||||
func (l *Log) Set(lines []string) {
|
||||
func (l *Log) Set(items dao.LogItems) {
|
||||
l.mx.Lock()
|
||||
defer l.mx.Unlock()
|
||||
|
||||
l.lines = lines
|
||||
l.fireLogChanged(lines)
|
||||
l.lines = items
|
||||
l.fireLogCleared()
|
||||
l.fireLogChanged(items)
|
||||
}
|
||||
|
||||
// ClearFilter resets the log filter if any.
|
||||
func (l *Log) ClearFilter() {
|
||||
log.Debug().Msgf("CLEARED!!")
|
||||
l.mx.RLock()
|
||||
defer l.mx.RUnlock()
|
||||
|
||||
l.filter = ""
|
||||
l.fireLogCleared()
|
||||
l.fireLogChanged(l.lines)
|
||||
}
|
||||
|
||||
|
|
@ -131,14 +149,9 @@ func (l *Log) Filter(q string) error {
|
|||
l.mx.RLock()
|
||||
defer l.mx.RUnlock()
|
||||
|
||||
log.Debug().Msgf("FILTER!")
|
||||
l.filter = q
|
||||
filtered, err := applyFilter(l.filter, l.lines)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.fireLogCleared()
|
||||
l.fireLogChanged(filtered)
|
||||
l.fireLogBuffChanged(l.lines)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -148,7 +161,7 @@ func (l *Log) load() error {
|
|||
ctx = context.WithValue(context.Background(), internal.KeyFactory, l.factory)
|
||||
ctx, l.cancelFn = context.WithCancel(ctx)
|
||||
|
||||
c := make(chan []byte, 10)
|
||||
c := make(dao.LogChan, 10)
|
||||
go l.updateLogs(ctx, c)
|
||||
|
||||
accessor, err := dao.AccessorFor(l.factory, l.gvr)
|
||||
|
|
@ -160,6 +173,7 @@ func (l *Log) load() error {
|
|||
return fmt.Errorf("Resource %s is not Loggable", l.gvr)
|
||||
}
|
||||
if err := logger.TailLogs(ctx, c, l.logOptions); err != nil {
|
||||
log.Error().Err(err).Msgf("Tail logs failed")
|
||||
if l.cancelFn != nil {
|
||||
l.cancelFn()
|
||||
}
|
||||
|
|
@ -171,14 +185,15 @@ func (l *Log) load() error {
|
|||
}
|
||||
|
||||
// Append adds a log line.
|
||||
func (l *Log) Append(line string) {
|
||||
if line == "" {
|
||||
func (l *Log) Append(line *dao.LogItem) {
|
||||
if line == nil || line.IsEmpty() {
|
||||
return
|
||||
}
|
||||
|
||||
l.mx.Lock()
|
||||
defer l.mx.Unlock()
|
||||
|
||||
l.logOptions.SinceTime = line.Timestamp
|
||||
if l.lines == nil {
|
||||
l.fireLogCleared()
|
||||
}
|
||||
|
|
@ -205,20 +220,20 @@ func (l *Log) Notify(timedOut bool) {
|
|||
}
|
||||
}
|
||||
|
||||
func (l *Log) updateLogs(ctx context.Context, c <-chan []byte) {
|
||||
func (l *Log) updateLogs(ctx context.Context, c dao.LogChan) {
|
||||
defer func() {
|
||||
log.Debug().Msgf("updateLogs view bailing out!")
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case bytes, ok := <-c:
|
||||
case item, ok := <-c:
|
||||
if !ok {
|
||||
log.Debug().Msgf("Closed channel detected. Bailing out...")
|
||||
l.Append(string(bytes))
|
||||
l.Append(item)
|
||||
l.Notify(false)
|
||||
return
|
||||
}
|
||||
l.Append(string(bytes))
|
||||
l.Append(item)
|
||||
var overflow bool
|
||||
l.mx.RLock()
|
||||
{
|
||||
|
|
@ -256,11 +271,11 @@ func (l *Log) RemoveListener(listener LogsListener) {
|
|||
}
|
||||
}
|
||||
|
||||
func applyFilter(q string, lines []string) ([]string, error) {
|
||||
func applyFilter(q string, lines dao.LogItems) (dao.LogItems, error) {
|
||||
if q == "" {
|
||||
return lines, nil
|
||||
}
|
||||
indexes, err := filter(q, lines)
|
||||
indexes, err := lines.Filter(q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -272,7 +287,7 @@ func applyFilter(q string, lines []string) ([]string, error) {
|
|||
if len(indexes) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
filtered := make([]string, 0, len(indexes))
|
||||
filtered := make(dao.LogItems, 0, len(indexes))
|
||||
for _, idx := range indexes {
|
||||
filtered = append(filtered, lines[idx])
|
||||
}
|
||||
|
|
@ -280,7 +295,7 @@ func applyFilter(q string, lines []string) ([]string, error) {
|
|||
return filtered, nil
|
||||
}
|
||||
|
||||
func (l *Log) fireLogBuffChanged(lines []string) {
|
||||
func (l *Log) fireLogBuffChanged(lines dao.LogItems) {
|
||||
filtered, err := applyFilter(l.filter, lines)
|
||||
if err != nil {
|
||||
l.fireLogError(err)
|
||||
|
|
@ -297,7 +312,7 @@ func (l *Log) fireLogError(err error) {
|
|||
}
|
||||
}
|
||||
|
||||
func (l *Log) fireLogChanged(lines []string) {
|
||||
func (l *Log) fireLogChanged(lines dao.LogItems) {
|
||||
for _, lis := range l.listeners {
|
||||
lis.LogChanged(lines)
|
||||
}
|
||||
|
|
@ -308,55 +323,3 @@ func (l *Log) fireLogCleared() {
|
|||
lis.LogCleared()
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
var fuzzyRx = regexp.MustCompile(`\A\-f`)
|
||||
|
||||
func isFuzzySelector(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
return fuzzyRx.MatchString(s)
|
||||
}
|
||||
|
||||
func filter(q string, lines []string) ([]int, error) {
|
||||
if q == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if isFuzzySelector(q) {
|
||||
return fuzzyFilter(strings.TrimSpace(q[2:]), lines), nil
|
||||
}
|
||||
indexes, err := filterLogs(q, lines)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Logs filter failed")
|
||||
return nil, err
|
||||
}
|
||||
return indexes, nil
|
||||
}
|
||||
|
||||
func fuzzyFilter(q string, lines []string) []int {
|
||||
matches := make([]int, 0, len(lines))
|
||||
mm := fuzzy.Find(q, lines)
|
||||
for _, m := range mm {
|
||||
matches = append(matches, m.Index)
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
func filterLogs(q string, lines []string) ([]int, error) {
|
||||
rx, err := regexp.Compile(`(?i)` + q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matches := make([]int, 0, len(lines))
|
||||
for i, l := range lines {
|
||||
if rx.MatchString(l) {
|
||||
matches = append(matches, i)
|
||||
}
|
||||
}
|
||||
|
||||
return matches, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ func TestLogFullBuffer(t *testing.T) {
|
|||
v := newTestView()
|
||||
m.AddListener(v)
|
||||
|
||||
data := make([]string, 0, 2*size)
|
||||
data := make(dao.LogItems, 0, 2*size)
|
||||
for i := 0; i < 2*size; i++ {
|
||||
data = append(data, "line"+strconv.Itoa(i))
|
||||
data = append(data, dao.NewLogItemFromString("line"+strconv.Itoa(i)))
|
||||
m.Append(data[i])
|
||||
}
|
||||
m.Notify(true)
|
||||
|
|
@ -47,8 +47,8 @@ func TestLogFilter(t *testing.T) {
|
|||
e: 2,
|
||||
},
|
||||
"regexp": {
|
||||
q: `\Apod-line-[1-3]{1}\z`,
|
||||
e: 3,
|
||||
q: `pod-line-[1-3]{1}`,
|
||||
e: 4,
|
||||
},
|
||||
"fuzzy": {
|
||||
q: `-f po-l1`,
|
||||
|
|
@ -67,21 +67,21 @@ func TestLogFilter(t *testing.T) {
|
|||
m.AddListener(v)
|
||||
|
||||
m.Filter(u.q)
|
||||
var data []string
|
||||
var data dao.LogItems
|
||||
for i := 0; i < size; i++ {
|
||||
data = append(data, fmt.Sprintf("pod-line-%d", i+1))
|
||||
data = append(data, dao.NewLogItemFromString(fmt.Sprintf("pod-line-%d", i+1)))
|
||||
m.Append(data[i])
|
||||
}
|
||||
|
||||
m.Notify(true)
|
||||
assert.Equal(t, 2, v.dataCalled)
|
||||
assert.Equal(t, 1, v.dataCalled)
|
||||
assert.Equal(t, 2, v.clearCalled)
|
||||
assert.Equal(t, 0, v.errCalled)
|
||||
assert.Equal(t, u.e, len(v.data))
|
||||
|
||||
m.ClearFilter()
|
||||
assert.Equal(t, 3, v.dataCalled)
|
||||
assert.Equal(t, 2, v.clearCalled)
|
||||
assert.Equal(t, 2, v.dataCalled)
|
||||
assert.Equal(t, 3, v.clearCalled)
|
||||
assert.Equal(t, 0, v.errCalled)
|
||||
assert.Equal(t, size, len(v.data))
|
||||
})
|
||||
|
|
@ -96,7 +96,7 @@ func TestLogStartStop(t *testing.T) {
|
|||
m.AddListener(v)
|
||||
|
||||
m.Start()
|
||||
data := []string{"line1", "line2"}
|
||||
data := dao.LogItems{dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2")}
|
||||
for _, d := range data {
|
||||
m.Append(d)
|
||||
}
|
||||
|
|
@ -118,7 +118,7 @@ func TestLogClear(t *testing.T) {
|
|||
v := newTestView()
|
||||
m.AddListener(v)
|
||||
|
||||
data := []string{"line1", "line2"}
|
||||
data := dao.LogItems{dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2")}
|
||||
for _, d := range data {
|
||||
m.Append(d)
|
||||
}
|
||||
|
|
@ -138,11 +138,11 @@ func TestLogBasic(t *testing.T) {
|
|||
v := newTestView()
|
||||
m.AddListener(v)
|
||||
|
||||
data := []string{"line1", "line2"}
|
||||
data := dao.LogItems{dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2")}
|
||||
m.Set(data)
|
||||
|
||||
assert.Equal(t, 1, v.dataCalled)
|
||||
assert.Equal(t, 0, v.clearCalled)
|
||||
assert.Equal(t, 1, v.clearCalled)
|
||||
assert.Equal(t, 0, v.errCalled)
|
||||
assert.Equal(t, data, v.data)
|
||||
}
|
||||
|
|
@ -153,21 +153,25 @@ func TestLogAppend(t *testing.T) {
|
|||
|
||||
v := newTestView()
|
||||
m.AddListener(v)
|
||||
m.Set([]string{"blah blah"})
|
||||
assert.Equal(t, []string{"blah blah"}, v.data)
|
||||
items := dao.LogItems{dao.NewLogItemFromString("blah blah")}
|
||||
m.Set(items)
|
||||
assert.Equal(t, items, v.data)
|
||||
|
||||
data := []string{"line1", "line2"}
|
||||
data := dao.LogItems{
|
||||
dao.NewLogItemFromString("line1"),
|
||||
dao.NewLogItemFromString("line2"),
|
||||
}
|
||||
for _, d := range data {
|
||||
m.Append(d)
|
||||
}
|
||||
assert.Equal(t, 1, v.dataCalled)
|
||||
assert.Equal(t, []string{"blah blah"}, v.data)
|
||||
assert.Equal(t, items, v.data)
|
||||
|
||||
m.Notify(true)
|
||||
assert.Equal(t, 2, v.dataCalled)
|
||||
assert.Equal(t, 0, v.clearCalled)
|
||||
assert.Equal(t, 1, v.clearCalled)
|
||||
assert.Equal(t, 0, v.errCalled)
|
||||
assert.Equal(t, append([]string{"blah blah"}, data...), v.data)
|
||||
assert.Equal(t, append(items, data...), v.data)
|
||||
}
|
||||
|
||||
func TestLogTimedout(t *testing.T) {
|
||||
|
|
@ -178,15 +182,20 @@ func TestLogTimedout(t *testing.T) {
|
|||
m.AddListener(v)
|
||||
|
||||
m.Filter("line1")
|
||||
data := []string{"line1", "line2", "line3", "line4"}
|
||||
data := dao.LogItems{
|
||||
dao.NewLogItemFromString("line1"),
|
||||
dao.NewLogItemFromString("line2"),
|
||||
dao.NewLogItemFromString("line3"),
|
||||
dao.NewLogItemFromString("line4"),
|
||||
}
|
||||
for _, d := range data {
|
||||
m.Append(d)
|
||||
}
|
||||
m.Notify(true)
|
||||
assert.Equal(t, 2, v.dataCalled)
|
||||
assert.Equal(t, 1, v.dataCalled)
|
||||
assert.Equal(t, 2, v.clearCalled)
|
||||
assert.Equal(t, 0, v.errCalled)
|
||||
assert.Equal(t, []string{"line1"}, v.data)
|
||||
assert.Equal(t, dao.LogItems{data[0]}, v.data)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
|
@ -203,7 +212,7 @@ func makeLogOpts(count int) dao.LogOptions {
|
|||
// ----------------------------------------------------------------------------
|
||||
|
||||
type testView struct {
|
||||
data []string
|
||||
data dao.LogItems
|
||||
dataCalled int
|
||||
clearCalled int
|
||||
errCalled int
|
||||
|
|
@ -213,13 +222,13 @@ func newTestView() *testView {
|
|||
return &testView{}
|
||||
}
|
||||
|
||||
func (t *testView) LogChanged(d []string) {
|
||||
func (t *testView) LogChanged(d dao.LogItems) {
|
||||
t.data = d
|
||||
t.dataCalled++
|
||||
}
|
||||
func (t *testView) LogCleared() {
|
||||
t.clearCalled++
|
||||
t.data = []string{}
|
||||
t.data = dao.LogItems{}
|
||||
}
|
||||
func (t *testView) LogFailed(err error) {
|
||||
fmt.Println("LogErr", err)
|
||||
|
|
|
|||
|
|
@ -290,7 +290,6 @@ func (t *Table) getMeta(ctx context.Context) (ResourceMeta, error) {
|
|||
func (t *Table) resourceMeta() ResourceMeta {
|
||||
meta, ok := Registry[t.gvr.String()]
|
||||
if !ok {
|
||||
log.Debug().Msgf("Resource %s not found in registry. Going generic!", t.gvr)
|
||||
meta = ResourceMeta{
|
||||
DAO: &dao.Table{},
|
||||
Renderer: &render.Generic{},
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/sahilm/fuzzy"
|
||||
)
|
||||
|
||||
|
|
@ -94,7 +95,7 @@ func (t *Text) filter(q string, lines []string) fuzzy.Matches {
|
|||
if q == "" {
|
||||
return nil
|
||||
}
|
||||
if isFuzzySelector(q) {
|
||||
if dao.IsFuzzySelector(q) {
|
||||
return t.fuzzyFilter(strings.TrimSpace(q[2:]), lines)
|
||||
}
|
||||
return t.rxFilter(q, lines)
|
||||
|
|
|
|||
|
|
@ -238,7 +238,6 @@ func (t *Tree) reconcile(ctx context.Context) error {
|
|||
func (t *Tree) resourceMeta() ResourceMeta {
|
||||
meta, ok := Registry[t.gvr.String()]
|
||||
if !ok {
|
||||
log.Debug().Msgf("Resource %s not found in registry. Going generic!", t.gvr)
|
||||
meta = ResourceMeta{
|
||||
DAO: &dao.Table{},
|
||||
Renderer: &render.Generic{},
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ type App struct {
|
|||
flash *model.Flash
|
||||
actions KeyActions
|
||||
views map[string]tview.Primitive
|
||||
cmdBuff *CmdBuff
|
||||
cmdBuff *model.FishBuff
|
||||
}
|
||||
|
||||
// NewApp returns a new app.
|
||||
|
|
@ -28,14 +28,14 @@ func NewApp(context string) *App {
|
|||
actions: make(KeyActions),
|
||||
Main: NewPages(),
|
||||
flash: model.NewFlash(model.DefaultFlashDelay),
|
||||
cmdBuff: NewCmdBuff(':', CommandBuff),
|
||||
cmdBuff: model.NewFishBuff(':', model.Command),
|
||||
}
|
||||
a.ReloadStyles(context)
|
||||
|
||||
a.views = map[string]tview.Primitive{
|
||||
"menu": NewMenu(a.Styles),
|
||||
"logo": NewLogo(a.Styles),
|
||||
"cmd": NewCommand(a.Styles),
|
||||
"cmd": NewCommand(a.Styles, a.cmdBuff),
|
||||
"crumbs": NewCrumbs(a.Styles),
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ func (a *App) Init() {
|
|||
func (a *App) BufferChanged(s string) {}
|
||||
|
||||
// BufferActive indicates the buff activity changed.
|
||||
func (a *App) BufferActive(state bool, _ BufferKind) {
|
||||
func (a *App) BufferActive(state bool, _ model.BufferKind) {
|
||||
flex, ok := a.Main.GetPrimitive("main").(*tview.Flex)
|
||||
if !ok {
|
||||
return
|
||||
|
|
@ -70,6 +70,8 @@ func (a *App) BufferActive(state bool, _ BufferKind) {
|
|||
a.Draw()
|
||||
}
|
||||
|
||||
func (a *App) SuggestionChanged(ss []string) {}
|
||||
|
||||
// StylesChanged notifies the skin changed.
|
||||
func (a *App) StylesChanged(s *config.Styles) {
|
||||
a.Main.SetBackgroundColor(s.BgColor())
|
||||
|
|
@ -129,7 +131,7 @@ func (a *App) GetCmd() string {
|
|||
}
|
||||
|
||||
// CmdBuff returns a cmd buffer.
|
||||
func (a *App) CmdBuff() *CmdBuff {
|
||||
func (a *App) CmdBuff() *model.FishBuff {
|
||||
return a.cmdBuff
|
||||
}
|
||||
|
||||
|
|
@ -190,6 +192,7 @@ func (a *App) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
a.cmdBuff.SetActive(true)
|
||||
a.cmdBuff.Clear()
|
||||
a.SetFocus(a.Cmd())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -207,6 +210,7 @@ func (a *App) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
func (a *App) escapeCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if a.cmdBuff.IsActive() {
|
||||
a.cmdBuff.Reset()
|
||||
a.SetFocus(a.Main.GetPrimitive("main"))
|
||||
}
|
||||
return evt
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,25 +4,29 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
const defaultPrompt = "%c> %s"
|
||||
const defaultPrompt = "%c> [::b]%s"
|
||||
|
||||
// Command captures users free from command input.
|
||||
type Command struct {
|
||||
*tview.TextView
|
||||
|
||||
activated bool
|
||||
icon rune
|
||||
text string
|
||||
styles *config.Styles
|
||||
activated bool
|
||||
icon rune
|
||||
text string
|
||||
styles *config.Styles
|
||||
model *model.FishBuff
|
||||
suggestions []string
|
||||
suggestionIndex int
|
||||
}
|
||||
|
||||
// NewCommand returns a new command view.
|
||||
func NewCommand(styles *config.Styles) *Command {
|
||||
c := Command{styles: styles, TextView: tview.NewTextView()}
|
||||
func NewCommand(styles *config.Styles, m *model.FishBuff) *Command {
|
||||
c := Command{styles: styles, TextView: tview.NewTextView(), model: m}
|
||||
c.SetWordWrap(true)
|
||||
c.SetWrap(true)
|
||||
c.SetDynamicColors(true)
|
||||
|
|
@ -31,10 +35,46 @@ func NewCommand(styles *config.Styles) *Command {
|
|||
c.SetBackgroundColor(styles.BgColor())
|
||||
c.SetTextColor(styles.FgColor())
|
||||
styles.AddListener(&c)
|
||||
c.SetInputCapture(c.keyboard)
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
func (c *Command) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
switch evt.Key() {
|
||||
case tcell.KeyEnter, tcell.KeyCtrlE:
|
||||
if c.suggestionIndex >= 0 {
|
||||
c.model.Set(c.text + c.suggestions[c.suggestionIndex])
|
||||
}
|
||||
case tcell.KeyCtrlW:
|
||||
c.model.Clear()
|
||||
case tcell.KeyTab, tcell.KeyDown:
|
||||
if c.text == "" || c.suggestionIndex < 0 {
|
||||
return evt
|
||||
}
|
||||
c.suggestionIndex++
|
||||
if c.suggestionIndex >= len(c.suggestions) {
|
||||
c.suggestionIndex = 0
|
||||
}
|
||||
c.suggest(c.model.String(), c.suggestions[c.suggestionIndex])
|
||||
case tcell.KeyBacktab, tcell.KeyUp:
|
||||
if c.text == "" || c.suggestionIndex < 0 {
|
||||
return evt
|
||||
}
|
||||
c.suggestionIndex--
|
||||
if c.suggestionIndex < 0 {
|
||||
c.suggestionIndex = len(c.suggestions) - 1
|
||||
}
|
||||
c.suggest(c.model.String(), c.suggestions[c.suggestionIndex])
|
||||
case tcell.KeyRight, tcell.KeyCtrlF:
|
||||
if c.suggestionIndex >= 0 {
|
||||
c.model.Set(c.model.String() + c.suggestions[c.suggestionIndex])
|
||||
c.suggestionIndex = -1
|
||||
}
|
||||
}
|
||||
return evt
|
||||
}
|
||||
|
||||
// StylesChanged notifies skin changed.
|
||||
func (c *Command) StylesChanged(s *config.Styles) {
|
||||
c.styles = s
|
||||
|
|
@ -60,6 +100,11 @@ func (c *Command) update(s string) {
|
|||
c.write(c.text)
|
||||
}
|
||||
|
||||
func (c *Command) suggest(text, suggestion string) {
|
||||
c.Clear()
|
||||
c.write(text + "[gray::-]" + suggestion)
|
||||
}
|
||||
|
||||
func (c *Command) write(s string) {
|
||||
fmt.Fprintf(c, defaultPrompt, c.icon, s)
|
||||
}
|
||||
|
|
@ -67,19 +112,28 @@ func (c *Command) write(s string) {
|
|||
// ----------------------------------------------------------------------------
|
||||
// Event Listener protocol...
|
||||
|
||||
// SuggestionChanged indicates the suggestions changed.
|
||||
func (c *Command) SuggestionChanged(ss []string) {
|
||||
c.suggestions, c.suggestionIndex = ss, 0
|
||||
if ss == nil {
|
||||
c.suggestionIndex = -1
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(c, "[gray::-]%s", ss[c.suggestionIndex])
|
||||
}
|
||||
|
||||
// BufferChanged indicates the buffer was changed.
|
||||
func (c *Command) BufferChanged(s string) {
|
||||
c.update(s)
|
||||
}
|
||||
|
||||
// BufferActive indicates the buff activity changed.
|
||||
func (c *Command) BufferActive(f bool, k BufferKind) {
|
||||
func (c *Command) BufferActive(f bool, k model.BufferKind) {
|
||||
if c.activated = f; f {
|
||||
c.SetBorder(true)
|
||||
c.SetTextColor(c.styles.FgColor())
|
||||
c.SetBorderColor(colorFor(k))
|
||||
c.icon = iconFor(k)
|
||||
// c.reset()
|
||||
c.activate()
|
||||
} else {
|
||||
c.SetBorder(false)
|
||||
|
|
@ -88,18 +142,18 @@ func (c *Command) BufferActive(f bool, k BufferKind) {
|
|||
}
|
||||
}
|
||||
|
||||
func colorFor(k BufferKind) tcell.Color {
|
||||
func colorFor(k model.BufferKind) tcell.Color {
|
||||
switch k {
|
||||
case CommandBuff:
|
||||
case model.Command:
|
||||
return tcell.ColorAqua
|
||||
default:
|
||||
return tcell.ColorSeaGreen
|
||||
}
|
||||
}
|
||||
|
||||
func iconFor(k BufferKind) rune {
|
||||
func iconFor(k model.BufferKind) rune {
|
||||
switch k {
|
||||
case CommandBuff:
|
||||
case model.Command:
|
||||
return '🐶'
|
||||
default:
|
||||
return '🐩'
|
||||
|
|
|
|||
|
|
@ -4,41 +4,40 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCmdNew(t *testing.T) {
|
||||
v := ui.NewCommand(config.NewStyles())
|
||||
model := model.NewFishBuff(':', model.Command)
|
||||
v := ui.NewCommand(config.NewStyles(), model)
|
||||
|
||||
buff := ui.NewCmdBuff(':', ui.CommandBuff)
|
||||
buff.AddListener(v)
|
||||
buff.Set("blee")
|
||||
model.AddListener(v)
|
||||
model.Set("blee")
|
||||
|
||||
assert.Equal(t, "\x00> blee\n", v.GetText(false))
|
||||
assert.Equal(t, "\x00> [::b]blee\n", v.GetText(false))
|
||||
}
|
||||
|
||||
func TestCmdUpdate(t *testing.T) {
|
||||
v := ui.NewCommand(config.NewStyles())
|
||||
model := model.NewFishBuff(':', model.Command)
|
||||
v := ui.NewCommand(config.NewStyles(), model)
|
||||
|
||||
buff := ui.NewCmdBuff(':', ui.CommandBuff)
|
||||
buff.AddListener(v)
|
||||
model.AddListener(v)
|
||||
model.Set("blee")
|
||||
model.Add('!')
|
||||
|
||||
buff.Set("blee")
|
||||
buff.Add('!')
|
||||
|
||||
assert.Equal(t, "\x00> blee!\n", v.GetText(false))
|
||||
assert.Equal(t, "\x00> [::b]blee!\n", v.GetText(false))
|
||||
assert.False(t, v.InCmdMode())
|
||||
}
|
||||
|
||||
func TestCmdMode(t *testing.T) {
|
||||
v := ui.NewCommand(config.NewStyles())
|
||||
|
||||
buff := ui.NewCmdBuff(':', ui.CommandBuff)
|
||||
buff.AddListener(v)
|
||||
model := model.NewFishBuff(':', model.Command)
|
||||
v := ui.NewCommand(config.NewStyles(), model)
|
||||
model.AddListener(v)
|
||||
|
||||
for _, f := range []bool{false, true} {
|
||||
buff.SetActive(f)
|
||||
model.SetActive(f)
|
||||
assert.Equal(t, f, v.InCmdMode())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ func initKeys() {
|
|||
|
||||
// Defines numeric keys for container actions
|
||||
const (
|
||||
Key0 int32 = iota + 48
|
||||
Key0 tcell.Key = iota + 48
|
||||
Key1
|
||||
Key2
|
||||
Key3
|
||||
|
|
@ -33,16 +33,16 @@ const (
|
|||
|
||||
// Defines numeric keys for container actions
|
||||
const (
|
||||
KeyShift0 int32 = 41
|
||||
KeyShift1 int32 = 33
|
||||
KeyShift2 int32 = 64
|
||||
KeyShift3 int32 = 35
|
||||
KeyShift4 int32 = 36
|
||||
KeyShift5 int32 = 37
|
||||
KeyShift6 int32 = 94
|
||||
KeyShift7 int32 = 38
|
||||
KeyShift8 int32 = 42
|
||||
KeyShift9 int32 = 40
|
||||
KeyShift0 tcell.Key = 41
|
||||
KeyShift1 tcell.Key = 33
|
||||
KeyShift2 tcell.Key = 64
|
||||
KeyShift3 tcell.Key = 35
|
||||
KeyShift4 tcell.Key = 36
|
||||
KeyShift5 tcell.Key = 37
|
||||
KeyShift6 tcell.Key = 94
|
||||
KeyShift7 tcell.Key = 38
|
||||
KeyShift8 tcell.Key = 42
|
||||
KeyShift9 tcell.Key = 40
|
||||
)
|
||||
|
||||
// Defines char keystrokes
|
||||
|
|
@ -110,7 +110,7 @@ const (
|
|||
)
|
||||
|
||||
// NumKeys tracks number keys.
|
||||
var NumKeys = map[int]int32{
|
||||
var NumKeys = map[int]tcell.Key{
|
||||
0: Key0,
|
||||
1: Key1,
|
||||
2: Key2,
|
||||
|
|
@ -124,16 +124,16 @@ var NumKeys = map[int]int32{
|
|||
}
|
||||
|
||||
func initNumbKeys() {
|
||||
tcell.KeyNames[tcell.Key(Key0)] = "0"
|
||||
tcell.KeyNames[tcell.Key(Key1)] = "1"
|
||||
tcell.KeyNames[tcell.Key(Key2)] = "2"
|
||||
tcell.KeyNames[tcell.Key(Key3)] = "3"
|
||||
tcell.KeyNames[tcell.Key(Key4)] = "4"
|
||||
tcell.KeyNames[tcell.Key(Key5)] = "5"
|
||||
tcell.KeyNames[tcell.Key(Key6)] = "6"
|
||||
tcell.KeyNames[tcell.Key(Key7)] = "7"
|
||||
tcell.KeyNames[tcell.Key(Key8)] = "8"
|
||||
tcell.KeyNames[tcell.Key(Key9)] = "9"
|
||||
tcell.KeyNames[Key0] = "0"
|
||||
tcell.KeyNames[Key1] = "1"
|
||||
tcell.KeyNames[Key2] = "2"
|
||||
tcell.KeyNames[Key3] = "3"
|
||||
tcell.KeyNames[Key4] = "4"
|
||||
tcell.KeyNames[Key5] = "5"
|
||||
tcell.KeyNames[Key6] = "6"
|
||||
tcell.KeyNames[Key7] = "7"
|
||||
tcell.KeyNames[Key8] = "8"
|
||||
tcell.KeyNames[Key9] = "9"
|
||||
}
|
||||
|
||||
func initStdKeys() {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
@ -30,10 +29,10 @@ func TestActionHints(t *testing.T) {
|
|||
}{
|
||||
"a": {
|
||||
aa: ui.KeyActions{
|
||||
ui.KeyB: ui.NewKeyAction("bleeB", nil, true),
|
||||
ui.KeyA: ui.NewKeyAction("bleeA", nil, true),
|
||||
tcell.Key(ui.Key0): ui.NewKeyAction("zero", nil, true),
|
||||
tcell.Key(ui.Key1): ui.NewKeyAction("one", nil, false),
|
||||
ui.KeyB: ui.NewKeyAction("bleeB", nil, true),
|
||||
ui.KeyA: ui.NewKeyAction("bleeA", nil, true),
|
||||
ui.Key0: ui.NewKeyAction("zero", nil, true),
|
||||
ui.Key1: ui.NewKeyAction("one", nil, false),
|
||||
},
|
||||
e: model.MenuHints{
|
||||
{Mnemonic: "0", Description: "zero", Visible: true},
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ type Table struct {
|
|||
actions KeyActions
|
||||
gvr client.GVR
|
||||
Path string
|
||||
cmdBuff *CmdBuff
|
||||
cmdBuff *model.CmdBuff
|
||||
styles *config.Styles
|
||||
viewSetting *config.ViewSetting
|
||||
sortCol SortColumn
|
||||
|
|
@ -56,7 +56,7 @@ func NewTable(gvr client.GVR) *Table {
|
|||
},
|
||||
gvr: gvr,
|
||||
actions: make(KeyActions),
|
||||
cmdBuff: NewCmdBuff('/', FilterBuff),
|
||||
cmdBuff: model.NewCmdBuff('/', model.Filter),
|
||||
sortCol: SortColumn{asc: true},
|
||||
}
|
||||
}
|
||||
|
|
@ -358,7 +358,7 @@ func (t *Table) filtered(data render.TableData) render.TableData {
|
|||
|
||||
q := t.cmdBuff.String()
|
||||
if IsFuzzySelector(q) {
|
||||
return fuzzyFilter(q[2:], t.NameColIndex(), filtered)
|
||||
return fuzzyFilter(q[2:], filtered)
|
||||
}
|
||||
|
||||
filtered, err := rxFilter(t.cmdBuff.String(), filtered)
|
||||
|
|
@ -371,7 +371,7 @@ func (t *Table) filtered(data render.TableData) render.TableData {
|
|||
}
|
||||
|
||||
// SearchBuff returns the associated command buffer.
|
||||
func (t *Table) SearchBuff() *CmdBuff {
|
||||
func (t *Table) SearchBuff() *model.CmdBuff {
|
||||
return t.cmdBuff
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -149,8 +149,7 @@ func rxFilter(q string, data render.TableData) (render.TableData, error) {
|
|||
Namespace: data.Namespace,
|
||||
}
|
||||
for _, re := range data.RowEvents {
|
||||
f := strings.Join(re.Row.Fields, " ")
|
||||
if rx.MatchString(f) {
|
||||
if rx.MatchString(re.Row.ID) {
|
||||
filtered.RowEvents = append(filtered.RowEvents, re)
|
||||
}
|
||||
}
|
||||
|
|
@ -158,10 +157,11 @@ func rxFilter(q string, data render.TableData) (render.TableData, error) {
|
|||
return filtered, nil
|
||||
}
|
||||
|
||||
func fuzzyFilter(q string, index int, data render.TableData) render.TableData {
|
||||
func fuzzyFilter(q string, data render.TableData) render.TableData {
|
||||
q = strings.TrimSpace(q)
|
||||
var ss []string
|
||||
for _, re := range data.RowEvents {
|
||||
ss = append(ss, re.Row.Fields[index])
|
||||
ss = append(ss, re.Row.ID)
|
||||
}
|
||||
|
||||
filtered := render.TableData{
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ type Tree struct {
|
|||
|
||||
actions KeyActions
|
||||
selectedItem string
|
||||
cmdBuff *CmdBuff
|
||||
cmdBuff *model.CmdBuff
|
||||
expandNodes bool
|
||||
Count int
|
||||
keyListener KeyListenerFunc
|
||||
|
|
@ -29,7 +29,7 @@ func NewTree() *Tree {
|
|||
TreeView: tview.NewTreeView(),
|
||||
expandNodes: true,
|
||||
actions: make(KeyActions),
|
||||
cmdBuff: NewCmdBuff('/', FilterBuff),
|
||||
cmdBuff: model.NewCmdBuff('/', model.Filter),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ func (t *Tree) ExpandNodes() bool {
|
|||
}
|
||||
|
||||
// CmdBuff returns the filter command.
|
||||
func (t *Tree) CmdBuff() *CmdBuff {
|
||||
func (t *Tree) CmdBuff() *model.CmdBuff {
|
||||
return t.cmdBuff
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ type buffL struct {
|
|||
func (b *buffL) BufferChanged(s string) {
|
||||
b.changed++
|
||||
}
|
||||
func (b *buffL) BufferActive(state bool, kind ui.BufferKind) {
|
||||
func (b *buffL) BufferActive(state bool, kind model.BufferKind) {
|
||||
b.active++
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
|
|
@ -96,6 +98,7 @@ func (a *App) Init(version string, rate int) error {
|
|||
if err := a.command.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
a.CmdBuff().SetSuggestionFn(a.suggestCommand())
|
||||
|
||||
a.clusterInfo().Init()
|
||||
|
||||
|
|
@ -104,9 +107,9 @@ func (a *App) Init(version string, rate int) error {
|
|||
|
||||
main := tview.NewFlex().SetDirection(tview.FlexRow)
|
||||
main.AddItem(a.statusIndicator(), 1, 1, false)
|
||||
main.AddItem(flash, 1, 1, false)
|
||||
main.AddItem(a.Content, 0, 10, true)
|
||||
main.AddItem(a.Crumbs(), 1, 1, false)
|
||||
main.AddItem(flash, 1, 1, false)
|
||||
|
||||
a.Main.AddPage("main", main, true, false)
|
||||
a.Main.AddPage("splash", ui.NewSplash(a.Styles, version), true, true)
|
||||
|
|
@ -115,6 +118,29 @@ func (a *App) Init(version string, rate int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (a *App) suggestCommand() func(s string) (entries sort.StringSlice) {
|
||||
return func(s string) (entries sort.StringSlice) {
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
for _, k := range a.command.alias.Aliases.Keys() {
|
||||
lok, los := strings.ToLower(k), strings.ToLower(s)
|
||||
if lok == los {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(lok, los) {
|
||||
entries = append(entries, strings.Replace(k, los, "", 1))
|
||||
}
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
entries = nil
|
||||
}
|
||||
entries.Sort()
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
key := evt.Key()
|
||||
if key == tcell.KeyRune {
|
||||
|
|
@ -442,6 +468,9 @@ func (a *App) helpCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
|
||||
func (a *App) aliasCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if a.CmdBuff().InCmdMode() {
|
||||
return evt
|
||||
}
|
||||
if _, ok := a.Content.GetPrimitive("main").(*Alias); ok {
|
||||
return evt
|
||||
}
|
||||
|
|
|
|||
|
|
@ -403,14 +403,14 @@ func (b *Browser) namespaceActions(aa ui.KeyActions) {
|
|||
return
|
||||
}
|
||||
b.namespaces = make(map[int]string, config.MaxFavoritesNS)
|
||||
aa[tcell.Key(ui.NumKeys[0])] = ui.NewKeyAction(client.NamespaceAll, b.switchNamespaceCmd, true)
|
||||
aa[ui.Key0] = ui.NewKeyAction(client.NamespaceAll, b.switchNamespaceCmd, true)
|
||||
b.namespaces[0] = client.NamespaceAll
|
||||
index := 1
|
||||
for _, ns := range b.app.Config.FavNamespaces() {
|
||||
if ns == client.NamespaceAll {
|
||||
continue
|
||||
}
|
||||
aa[tcell.Key(ui.NumKeys[index])] = ui.NewKeyAction(ns, b.switchNamespaceCmd, true)
|
||||
aa[ui.NumKeys[index]] = ui.NewKeyAction(ns, b.switchNamespaceCmd, true)
|
||||
b.namespaces[index] = ns
|
||||
index++
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import (
|
|||
"github.com/derailed/k9s/internal/render"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Chart represents a helm chart view.
|
||||
|
|
@ -36,18 +35,8 @@ func (c *Chart) chartContext(ctx context.Context) context.Context {
|
|||
func (c *Chart) bindKeys(aa ui.KeyActions) {
|
||||
aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)
|
||||
aa.Add(ui.KeyActions{
|
||||
ui.KeyB: ui.NewKeyAction("Blee", c.bleeCmd, true),
|
||||
ui.KeyShiftN: ui.NewKeyAction("Sort Name", c.GetTable().SortColCmd(nameCol, true), false),
|
||||
ui.KeyShiftS: ui.NewKeyAction("Sort Status", c.GetTable().SortColCmd(statusCol, true), false),
|
||||
ui.KeyShiftA: ui.NewKeyAction("Sort Age", c.GetTable().SortColCmd(ageCol, true), false),
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Chart) bleeCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
path := c.GetTable().GetSelectedItem()
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
log.Debug().Msgf("BLEE CMD %q", path)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,6 @@ func (c *ClusterInfo) ClusterInfoUpdated(data model.ClusterMeta) {
|
|||
|
||||
// ClusterInfoChanged notifies the cluster meta was changed.
|
||||
func (c *ClusterInfo) ClusterInfoChanged(prev, curr model.ClusterMeta) {
|
||||
// BOZO!!
|
||||
c.app.QueueUpdate(func() {
|
||||
c.Clear()
|
||||
c.layout()
|
||||
|
|
|
|||
|
|
@ -116,7 +116,6 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
return evt
|
||||
}
|
||||
|
||||
log.Debug().Msgf("CONTAINER-SEL %q", path)
|
||||
if _, ok := c.App().factory.ForwarderFor(fwFQN(c.GetTable().Path, path)); ok {
|
||||
c.App().Flash().Err(fmt.Errorf("A PortForward already exist on container %s", c.GetTable().Path))
|
||||
return nil
|
||||
|
|
@ -126,7 +125,6 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
if !ok {
|
||||
return nil
|
||||
}
|
||||
log.Debug().Msgf("CONTAINER-PORTS %#v", ports)
|
||||
ShowPortForwards(c, c.GetTable().Path, ports, startFwdCB)
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ type Details struct {
|
|||
actions ui.KeyActions
|
||||
app *App
|
||||
title, subject string
|
||||
cmdBuff *ui.CmdBuff
|
||||
cmdBuff *model.CmdBuff
|
||||
model *model.Text
|
||||
currentRegion, maxRegions int
|
||||
searchable bool
|
||||
|
|
@ -37,7 +37,7 @@ func NewDetails(app *App, title, subject string, searchable bool) *Details {
|
|||
title: title,
|
||||
subject: subject,
|
||||
actions: make(ui.KeyActions),
|
||||
cmdBuff: ui.NewCmdBuff('/', ui.FilterBuff),
|
||||
cmdBuff: model.NewCmdBuff('/', model.Filter),
|
||||
model: model.NewText(),
|
||||
searchable: searchable,
|
||||
}
|
||||
|
|
@ -103,7 +103,7 @@ func (d *Details) TextFiltered(lines []string, matches fuzzy.Matches) {
|
|||
func (d *Details) BufferChanged(s string) {}
|
||||
|
||||
// BufferActive indicates the buff activity changed.
|
||||
func (d *Details) BufferActive(state bool, k ui.BufferKind) {
|
||||
func (d *Details) BufferActive(state bool, k model.BufferKind) {
|
||||
d.app.BufferActive(state, k)
|
||||
}
|
||||
|
||||
|
|
@ -162,14 +162,12 @@ func (d *Details) StylesChanged(s *config.Styles) {
|
|||
d.SetBackgroundColor(d.app.Styles.BgColor())
|
||||
d.SetTextColor(d.app.Styles.FgColor())
|
||||
d.SetBorderFocusColor(d.app.Styles.Frame().Border.FocusColor.Color())
|
||||
|
||||
d.TextChanged(d.model.Peek())
|
||||
}
|
||||
|
||||
// Update updates the view content.
|
||||
func (d *Details) Update(buff string) *Details {
|
||||
d.model.SetText(buff)
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
|
|
@ -293,6 +291,7 @@ func (d *Details) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
} else {
|
||||
d.app.Flash().Infof("Log %s saved successfully!", path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -301,6 +300,7 @@ func (d *Details) cpCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
if err := clipboard.WriteAll(d.GetText(true)); err != nil {
|
||||
d.app.Flash().Err(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -321,6 +321,5 @@ func (d *Details) updateTitle() {
|
|||
search += fmt.Sprintf("[%d:%d]", d.currentRegion+1, d.maxRegions)
|
||||
}
|
||||
fmat += fmt.Sprintf(ui.SearchFmt, search)
|
||||
|
||||
d.SetTitle(ui.SkinTitle(fmat, d.app.Styles.Frame()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/color"
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
|
|
@ -21,10 +22,10 @@ import (
|
|||
|
||||
const (
|
||||
logTitle = "logs"
|
||||
logMessage = "[:orange:b]Waiting for logs...[::]"
|
||||
logCoFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:bg:-]) "
|
||||
logFmt = " Logs([fg:bg:]%s) "
|
||||
flushTimeout = 200 * time.Millisecond
|
||||
logMessage = "Waiting for logs..."
|
||||
logFmt = " Logs([hilite:bg:]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] "
|
||||
logCoFmt = " Logs([hilite:bg:]%s:[hilite:bg:b]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] "
|
||||
flushTimeout = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
// Log represents a generic log viewer.
|
||||
|
|
@ -35,9 +36,7 @@ type Log struct {
|
|||
logs *Details
|
||||
indicator *LogIndicator
|
||||
ansiWriter io.Writer
|
||||
cmdBuff *ui.CmdBuff
|
||||
model *model.Log
|
||||
counts int
|
||||
}
|
||||
|
||||
var _ model.Component = (*Log)(nil)
|
||||
|
|
@ -45,15 +44,18 @@ var _ model.Component = (*Log)(nil)
|
|||
// NewLog returns a new viewer.
|
||||
func NewLog(gvr client.GVR, path, co string, prev bool) *Log {
|
||||
l := Log{
|
||||
Flex: tview.NewFlex(),
|
||||
cmdBuff: ui.NewCmdBuff('/', ui.FilterBuff),
|
||||
model: model.NewLog(gvr, buildLogOpts(path, co, prev, true, config.DefaultLoggerTailCount), flushTimeout),
|
||||
Flex: tview.NewFlex(),
|
||||
model: model.NewLog(
|
||||
gvr,
|
||||
buildLogOpts(path, co, prev, true, config.DefaultLoggerTailCount),
|
||||
flushTimeout,
|
||||
),
|
||||
}
|
||||
|
||||
return &l
|
||||
}
|
||||
|
||||
// Init initialiazes the viewer.
|
||||
// Init initializes the viewer.
|
||||
func (l *Log) Init(ctx context.Context) (err error) {
|
||||
if l.app, err = extractApp(ctx); err != nil {
|
||||
return err
|
||||
|
|
@ -61,7 +63,6 @@ func (l *Log) Init(ctx context.Context) (err error) {
|
|||
l.model.Configure(l.app.Config.K9s.Logger)
|
||||
|
||||
l.SetBorder(true)
|
||||
l.SetBorderPadding(0, 0, 1, 1)
|
||||
l.SetDirection(tview.FlexRow)
|
||||
|
||||
l.indicator = NewLogIndicator(l.app.Config, l.app.Styles)
|
||||
|
|
@ -72,14 +73,15 @@ func (l *Log) Init(ctx context.Context) (err error) {
|
|||
if err = l.logs.Init(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
l.logs.SetBorderPadding(0, 0, 1, 1)
|
||||
l.logs.SetText(logMessage)
|
||||
l.logs.SetWrap(false)
|
||||
l.logs.SetMaxBuffer(l.app.Config.K9s.Logger.BufferSize)
|
||||
l.logs.cmdBuff.AddListener(l)
|
||||
|
||||
l.ansiWriter = tview.ANSIWriter(l.logs, l.app.Styles.Views().Log.FgColor.String(), l.app.Styles.Views().Log.BgColor.String())
|
||||
l.AddItem(l.logs, 0, 1, true)
|
||||
l.bindKeys()
|
||||
l.logs.SetInputCapture(l.keyboard)
|
||||
|
||||
l.StylesChanged(l.app.Styles)
|
||||
l.app.Styles.AddListener(l)
|
||||
|
|
@ -89,15 +91,11 @@ func (l *Log) Init(ctx context.Context) (err error) {
|
|||
l.model.AddListener(l)
|
||||
l.updateTitle()
|
||||
|
||||
l.cmdBuff.AddListener(l.app.Cmd())
|
||||
l.cmdBuff.AddListener(l)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogCleared clears the logs.
|
||||
func (l *Log) LogCleared() {
|
||||
l.counts = 0
|
||||
l.app.QueueUpdateDraw(func() {
|
||||
l.logs.Clear()
|
||||
l.logs.ScrollTo(0, 0)
|
||||
|
|
@ -108,21 +106,30 @@ func (l *Log) LogCleared() {
|
|||
func (l *Log) LogFailed(err error) {
|
||||
l.app.QueueUpdateDraw(func() {
|
||||
l.app.Flash().Err(err)
|
||||
if l.logs.GetText(true) == logMessage {
|
||||
l.logs.Clear()
|
||||
}
|
||||
l.write(color.Colorize(err.Error(), color.Red))
|
||||
})
|
||||
}
|
||||
|
||||
// LogChanged updates the logs.
|
||||
func (l *Log) LogChanged(lines []string) {
|
||||
func (l *Log) LogChanged(lines dao.LogItems) {
|
||||
l.app.QueueUpdateDraw(func() {
|
||||
l.Flush(lines)
|
||||
})
|
||||
}
|
||||
|
||||
// BufferChanged indicates the buffer was changed.
|
||||
func (l *Log) BufferChanged(s string) {}
|
||||
func (l *Log) BufferChanged(s string) {
|
||||
if err := l.model.Filter(l.logs.cmdBuff.String()); err != nil {
|
||||
l.app.Flash().Err(err)
|
||||
}
|
||||
l.updateTitle()
|
||||
}
|
||||
|
||||
// BufferActive indicates the buff activity changed.
|
||||
func (l *Log) BufferActive(state bool, k ui.BufferKind) {
|
||||
func (l *Log) BufferActive(state bool, k model.BufferKind) {
|
||||
l.app.BufferActive(state, k)
|
||||
}
|
||||
|
||||
|
|
@ -151,7 +158,6 @@ func (l *Log) ExtraHints() map[string]string {
|
|||
// Start runs the component.
|
||||
func (l *Log) Start() {
|
||||
l.model.Start()
|
||||
l.app.SetFocus(l)
|
||||
}
|
||||
|
||||
// Stop terminates the component.
|
||||
|
|
@ -159,8 +165,8 @@ func (l *Log) Stop() {
|
|||
l.model.Stop()
|
||||
l.model.RemoveListener(l)
|
||||
l.app.Styles.RemoveListener(l)
|
||||
l.cmdBuff.RemoveListener(l)
|
||||
l.cmdBuff.RemoveListener(l.app.Cmd())
|
||||
l.logs.cmdBuff.RemoveListener(l)
|
||||
l.logs.cmdBuff.RemoveListener(l.app.Cmd())
|
||||
}
|
||||
|
||||
// Name returns the component name.
|
||||
|
|
@ -168,43 +174,33 @@ func (l *Log) Name() string { return logTitle }
|
|||
|
||||
func (l *Log) bindKeys() {
|
||||
l.logs.Actions().Set(ui.KeyActions{
|
||||
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false),
|
||||
tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, true),
|
||||
ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true),
|
||||
ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.ToggleAutoScrollCmd, true),
|
||||
ui.KeyF: ui.NewKeyAction("FullScreen", l.fullScreenCmd, true),
|
||||
ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.textWrapCmd, true),
|
||||
tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true),
|
||||
ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", l.activateCmd, false),
|
||||
tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", l.resetCmd, false),
|
||||
tcell.KeyBackspace2: ui.NewSharedKeyAction("Erase", l.eraseCmd, false),
|
||||
tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", l.eraseCmd, false),
|
||||
tcell.KeyDelete: ui.NewSharedKeyAction("Erase", l.eraseCmd, false),
|
||||
ui.Key0: ui.NewKeyAction("all", l.sinceCmd(-1), true),
|
||||
ui.Key1: ui.NewKeyAction("1m", l.sinceCmd(60), true),
|
||||
ui.Key2: ui.NewKeyAction("5m", l.sinceCmd(5*60), true),
|
||||
ui.Key3: ui.NewKeyAction("15m", l.sinceCmd(15*60), true),
|
||||
ui.Key4: ui.NewKeyAction("30m", l.sinceCmd(30*60), true),
|
||||
ui.Key5: ui.NewKeyAction("1h", l.sinceCmd(60*60), true),
|
||||
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false),
|
||||
ui.KeyA: ui.NewKeyAction("Apply", l.applyCmd, true),
|
||||
ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true),
|
||||
ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleAutoScrollCmd, true),
|
||||
ui.KeyF: ui.NewKeyAction("FullScreen", l.toggleFullScreenCmd, true),
|
||||
ui.KeyT: ui.NewKeyAction("Toggle Timestamp", l.toggleTimestampCmd, true),
|
||||
ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.toggleTextWrapCmd, true),
|
||||
tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true),
|
||||
})
|
||||
}
|
||||
|
||||
func (l *Log) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
key := evt.Key()
|
||||
if key == tcell.KeyUp || key == tcell.KeyDown {
|
||||
return evt
|
||||
}
|
||||
if key == tcell.KeyRune {
|
||||
if l.cmdBuff.IsActive() {
|
||||
l.cmdBuff.Add(evt.Rune())
|
||||
if err := l.model.Filter(l.cmdBuff.String()); err != nil {
|
||||
l.app.Flash().Err(err)
|
||||
}
|
||||
l.updateTitle()
|
||||
return nil
|
||||
}
|
||||
key = extractKey(evt)
|
||||
func (l *Log) SendStrokes(s string) {
|
||||
for _, r := range s {
|
||||
l.logs.keyboard(tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone))
|
||||
}
|
||||
}
|
||||
|
||||
if a, ok := l.logs.Actions()[key]; ok {
|
||||
return a.Action(evt)
|
||||
func (l *Log) SendKeys(kk ...tcell.Key) {
|
||||
for _, k := range kk {
|
||||
l.logs.keyboard(tcell.NewEventKey(k, ' ', tcell.ModNone))
|
||||
}
|
||||
|
||||
return evt
|
||||
}
|
||||
|
||||
// Indicator returns the scroll mode viewer.
|
||||
|
|
@ -213,19 +209,26 @@ func (l *Log) Indicator() *LogIndicator {
|
|||
}
|
||||
|
||||
func (l *Log) updateTitle() {
|
||||
var fmat string
|
||||
sinceSeconds, since := l.model.SinceSeconds(), "all"
|
||||
if sinceSeconds > 0 && sinceSeconds < 60*60 {
|
||||
since = fmt.Sprintf("%dm", sinceSeconds/60)
|
||||
}
|
||||
if sinceSeconds >= 60*60 {
|
||||
since = fmt.Sprintf("%dh", sinceSeconds/(60*60))
|
||||
}
|
||||
var title string
|
||||
path, co := l.model.GetPath(), l.model.GetContainer()
|
||||
if co == "" {
|
||||
fmat = ui.SkinTitle(fmt.Sprintf(logFmt, path), l.app.Styles.Frame())
|
||||
title = ui.SkinTitle(fmt.Sprintf(logFmt, path, since), l.app.Styles.Frame())
|
||||
} else {
|
||||
fmat = ui.SkinTitle(fmt.Sprintf(logCoFmt, path, co), l.app.Styles.Frame())
|
||||
title = ui.SkinTitle(fmt.Sprintf(logCoFmt, path, co, since), l.app.Styles.Frame())
|
||||
}
|
||||
|
||||
buff := l.cmdBuff.String()
|
||||
buff := l.logs.cmdBuff.String()
|
||||
if buff != "" {
|
||||
fmat += ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), l.app.Styles.Frame())
|
||||
title += ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), l.app.Styles.Frame())
|
||||
}
|
||||
l.SetTitle(fmat)
|
||||
l.SetTitle(title)
|
||||
}
|
||||
|
||||
// Logs returns the log viewer.
|
||||
|
|
@ -238,43 +241,52 @@ func (l *Log) write(lines string) {
|
|||
}
|
||||
|
||||
// Flush write logs to viewer.
|
||||
func (l *Log) Flush(lines []string) {
|
||||
l.write(strings.Join(lines, "\n"))
|
||||
l.indicator.Refresh()
|
||||
func (l *Log) Flush(lines dao.LogItems) {
|
||||
defer func(t time.Time) {
|
||||
log.Debug().Msgf("FLUSH %d--%v", len(lines), time.Since(t))
|
||||
}(time.Now())
|
||||
|
||||
showTime := l.Indicator().showTime
|
||||
ll := make([]string, len(lines))
|
||||
for i, line := range lines {
|
||||
ll[i] = string(line.Render(showTime))
|
||||
}
|
||||
l.write(strings.Join(ll, "\n"))
|
||||
l.logs.ScrollToEnd()
|
||||
l.indicator.Refresh()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Actions()...
|
||||
|
||||
func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !l.cmdBuff.IsActive() {
|
||||
return evt
|
||||
func (l *Log) sinceCmd(a int) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
opts := l.model.LogOptions()
|
||||
opts.SinceSeconds = int64(a)
|
||||
l.model.SetLogOptions(opts)
|
||||
l.updateTitle()
|
||||
return nil
|
||||
}
|
||||
l.cmdBuff.SetActive(false)
|
||||
if err := l.model.Filter(l.cmdBuff.String()); err != nil {
|
||||
l.app.Flash().Err(err)
|
||||
}
|
||||
l.updateTitle()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
func (l *Log) applyCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if l.app.InCmdMode() {
|
||||
return evt
|
||||
}
|
||||
l.cmdBuff.SetActive(true)
|
||||
|
||||
ShowLogs(l.app, "blee", l.filterLogs)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !l.cmdBuff.IsActive() {
|
||||
return nil
|
||||
func (l *Log) filterLogs(path string, opts dao.LogOptions) {
|
||||
}
|
||||
|
||||
func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !l.logs.cmdBuff.IsActive() {
|
||||
return evt
|
||||
}
|
||||
l.cmdBuff.Delete()
|
||||
if err := l.model.Filter(l.cmdBuff.String()); err != nil {
|
||||
l.logs.cmdBuff.SetActive(false)
|
||||
if err := l.model.Filter(l.logs.cmdBuff.String()); err != nil {
|
||||
l.app.Flash().Err(err)
|
||||
}
|
||||
l.updateTitle()
|
||||
|
|
@ -282,22 +294,6 @@ func (l *Log) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !l.cmdBuff.InCmdMode() {
|
||||
l.cmdBuff.Reset()
|
||||
return l.app.PrevCmd(evt)
|
||||
}
|
||||
|
||||
if l.cmdBuff.String() != "" {
|
||||
l.model.ClearFilter()
|
||||
}
|
||||
l.cmdBuff.SetActive(false)
|
||||
l.cmdBuff.Reset()
|
||||
l.updateTitle()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveCmd dumps the logs to file.
|
||||
func (l *Log) SaveCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if path, err := saveData(l.app.Config.K9s.CurrentCluster, l.model.GetPath(), l.logs.GetText(true)); err != nil {
|
||||
|
|
@ -346,14 +342,33 @@ func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) textWrapCmd(*tcell.EventKey) *tcell.EventKey {
|
||||
func (l *Log) toggleTimestampCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if l.app.InCmdMode() {
|
||||
return evt
|
||||
}
|
||||
|
||||
l.indicator.ToggleTimestamp()
|
||||
l.model.Refresh()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) toggleTextWrapCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if l.app.InCmdMode() {
|
||||
return evt
|
||||
}
|
||||
|
||||
l.indicator.ToggleTextWrap()
|
||||
l.logs.SetWrap(l.indicator.textWrap)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToggleAutoScrollCmd toggles autoscroll status.
|
||||
func (l *Log) ToggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
func (l *Log) toggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if l.app.InCmdMode() {
|
||||
return evt
|
||||
}
|
||||
|
||||
l.indicator.ToggleAutoScroll()
|
||||
if l.indicator.AutoScroll() {
|
||||
l.model.Start()
|
||||
|
|
@ -363,34 +378,23 @@ func (l *Log) ToggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) fullScreenCmd(*tcell.EventKey) *tcell.EventKey {
|
||||
func (l *Log) toggleFullScreenCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if l.app.InCmdMode() {
|
||||
return evt
|
||||
}
|
||||
l.indicator.ToggleFullScreen()
|
||||
l.goFullScreen()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) goFullScreen() {
|
||||
sidePadding := 1
|
||||
if l.indicator.FullScreen() {
|
||||
sidePadding = 0
|
||||
}
|
||||
l.SetFullScreen(l.indicator.FullScreen())
|
||||
l.Box.SetBorder(!l.indicator.FullScreen())
|
||||
l.Flex.SetBorderPadding(0, 0, sidePadding, sidePadding)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
// AsKey converts rune to keyboard key.,
|
||||
func extractKey(evt *tcell.EventKey) tcell.Key {
|
||||
key := tcell.Key(evt.Rune())
|
||||
if evt.Modifiers() == tcell.ModAlt {
|
||||
key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers()))
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func buildLogOpts(path, co string, prevLogs, showTime bool, tailLineCount int) dao.LogOptions {
|
||||
return dao.LogOptions{
|
||||
Path: path,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/tview"
|
||||
)
|
||||
|
||||
const logKey = "logs"
|
||||
|
||||
// LogCB represents a log callback function.
|
||||
type LogCB func(path string, opts dao.LogOptions)
|
||||
|
||||
// ShowLogs pops a port forwarding configuration dialog.
|
||||
func ShowLogs(a *App, path string, applyFn LogCB) {
|
||||
styles := a.Styles
|
||||
|
||||
f := tview.NewForm()
|
||||
f.SetItemPadding(0)
|
||||
f.SetButtonsAlign(tview.AlignCenter).
|
||||
SetButtonBackgroundColor(styles.BgColor()).
|
||||
SetButtonTextColor(styles.FgColor()).
|
||||
SetLabelColor(styles.K9s.Info.FgColor.Color()).
|
||||
SetFieldTextColor(styles.K9s.Info.SectionColor.Color())
|
||||
|
||||
secs, start, in, out, container := "5", time.Now().String(), "", "", ""
|
||||
f.AddInputField("Container:", container, 0, nil, func(v string) {
|
||||
container = v
|
||||
})
|
||||
f.AddInputField("Since Seconds:", secs, 0, nil, func(v string) {
|
||||
secs = v
|
||||
})
|
||||
f.AddInputField("Since Time:", start, 0, nil, func(v string) {
|
||||
start = v
|
||||
})
|
||||
f.AddInputField("Filter In:", in, 0, nil, func(v string) {
|
||||
in = v
|
||||
})
|
||||
f.AddInputField("Filter Out:", out, 0, nil, func(v string) {
|
||||
out = v
|
||||
})
|
||||
|
||||
pages := a.Content.Pages
|
||||
|
||||
f.AddButton("Apply", func() {
|
||||
s, _ := strconv.Atoi(secs)
|
||||
opts := dao.LogOptions{
|
||||
SinceTime: start,
|
||||
SinceSeconds: int64(s),
|
||||
In: in,
|
||||
Out: out,
|
||||
}
|
||||
applyFn(path, opts)
|
||||
})
|
||||
f.AddButton("Dismiss", func() {
|
||||
DismissLogs(a, pages)
|
||||
})
|
||||
|
||||
modal := tview.NewModalForm(fmt.Sprintf("<Configure Logs for %s>", path), f)
|
||||
modal.SetDoneFunc(func(_ int, b string) {
|
||||
DismissLogs(a, pages)
|
||||
})
|
||||
|
||||
pages.AddPage(logKey, modal, false, true)
|
||||
pages.ShowPage(logKey)
|
||||
a.SetFocus(pages.GetPrimitive(logKey))
|
||||
}
|
||||
|
||||
// DismissLogs dismiss the dialog.
|
||||
func DismissLogs(a *App, p *ui.Pages) {
|
||||
p.RemovePage(logKey)
|
||||
a.SetFocus(p.CurrentPage().Item)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
|
@ -27,13 +27,20 @@ func NewLogIndicator(cfg *config.Config, styles *config.Styles) *LogIndicator {
|
|||
scrollStatus: 1,
|
||||
fullScreen: cfg.K9s.FullScreenLogs,
|
||||
}
|
||||
l.SetBackgroundColor(styles.Views().Log.BgColor.Color())
|
||||
l.SetTextAlign(tview.AlignRight)
|
||||
l.StylesChanged(styles)
|
||||
styles.AddListener(&l)
|
||||
l.SetTextAlign(tview.AlignCenter)
|
||||
l.SetDynamicColors(true)
|
||||
|
||||
return &l
|
||||
}
|
||||
|
||||
// StylesChanged notifies listener the skin changed.
|
||||
func (l *LogIndicator) StylesChanged(styles *config.Styles) {
|
||||
l.SetBackgroundColor(styles.K9s.Views.Log.Indicator.BgColor.Color())
|
||||
l.SetTextColor(styles.K9s.Views.Log.Indicator.FgColor.Color())
|
||||
}
|
||||
|
||||
// AutoScroll reports the current scrolling status.
|
||||
func (l *LogIndicator) AutoScroll() bool {
|
||||
return atomic.LoadInt32(&l.scrollStatus) == 1
|
||||
|
|
@ -86,8 +93,7 @@ func (l *LogIndicator) Refresh() {
|
|||
l.Clear()
|
||||
l.update("Autoscroll: " + l.onOff(l.AutoScroll()))
|
||||
l.update("FullScreen: " + l.onOff(l.fullScreen))
|
||||
// BOZO!! log timestamp
|
||||
// l.update("Timestamp: " + l.onOff(l.showTime))
|
||||
l.update("Timestamps: " + l.onOff(l.showTime))
|
||||
l.update("Wrap: " + l.onOff(l.textWrap))
|
||||
}
|
||||
|
||||
|
|
@ -99,6 +105,5 @@ func (l *LogIndicator) onOff(b bool) string {
|
|||
}
|
||||
|
||||
func (l *LogIndicator) update(status string) {
|
||||
fg, bg := l.styles.Frame().Crumb.FgColor, l.styles.Frame().Crumb.ActiveColor
|
||||
fmt.Fprintf(l, "[%s:%s:b] %-15s ", fg, bg, status)
|
||||
fmt.Fprintf(l, "[::b]%-20s", status)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@ func TestLogIndicatorRefresh(t *testing.T) {
|
|||
v := view.NewLogIndicator(config.NewConfig(nil), defaults)
|
||||
v.Refresh()
|
||||
|
||||
assert.Equal(t, "[black:orange:b] Autoscroll: On [black:orange:b] FullScreen: Off [black:orange:b] Wrap: Off \n", v.GetText(false))
|
||||
assert.Equal(t, "[::b]Autoscroll: On [::b]FullScreen: Off [::b]Timestamps: Off [::b]Wrap: Off \n", v.GetText(false))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLogAutoScroll(t *testing.T) {
|
||||
v := NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
|
||||
v.Init(makeContext())
|
||||
v.GetModel().Set(dao.LogItems{dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo")})
|
||||
v.GetModel().Notify(true)
|
||||
|
||||
assert.Equal(t, 14, len(v.Hints()))
|
||||
|
||||
v.toggleAutoScrollCmd(nil)
|
||||
assert.Equal(t, "Autoscroll: Off FullScreen: Off Timestamps: Off Wrap: Off ", v.Indicator().GetText(true))
|
||||
}
|
||||
|
||||
func TestLogViewNav(t *testing.T) {
|
||||
v := NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
|
||||
v.Init(makeContext())
|
||||
|
||||
var buff dao.LogItems
|
||||
for i := 0; i < 100; i++ {
|
||||
buff = append(buff, dao.NewLogItemFromString(fmt.Sprintf("line-%d\n", i)))
|
||||
}
|
||||
v.GetModel().Set(buff)
|
||||
v.toggleAutoScrollCmd(nil)
|
||||
|
||||
r, _ := v.Logs().GetScrollOffset()
|
||||
assert.Equal(t, 0, r)
|
||||
}
|
||||
|
||||
func TestLogViewClear(t *testing.T) {
|
||||
v := NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
|
||||
v.Init(makeContext())
|
||||
|
||||
v.toggleAutoScrollCmd(nil)
|
||||
v.Logs().SetText("blee\nblah")
|
||||
v.Logs().Clear()
|
||||
|
||||
assert.Equal(t, "", v.Logs().GetText(true))
|
||||
}
|
||||
|
||||
func TestLogTimestamp(t *testing.T) {
|
||||
l := NewLog(client.NewGVR("test"), "fred/blee", "c1", false)
|
||||
l.Init(makeContext())
|
||||
buff := dao.LogItems{
|
||||
&dao.LogItem{
|
||||
Pod: "fred/blee",
|
||||
Container: "c1",
|
||||
Timestamp: "ttt",
|
||||
Bytes: []byte("Testing 1, 2, 3"),
|
||||
},
|
||||
}
|
||||
var list logList
|
||||
l.GetModel().AddListener(&list)
|
||||
l.GetModel().Set(buff)
|
||||
l.SendKeys(ui.KeyT)
|
||||
l.Logs().Clear()
|
||||
l.Flush(buff)
|
||||
|
||||
assert.Equal(t, fmt.Sprintf("%-30s %s", "ttt", "fred/blee:c1 Testing 1, 2, 3\n"), l.Logs().GetText(true))
|
||||
assert.Equal(t, 2, list.change)
|
||||
assert.Equal(t, 2, list.clear)
|
||||
assert.Equal(t, 0, list.fail)
|
||||
}
|
||||
|
||||
func TestLogFilter(t *testing.T) {
|
||||
l := NewLog(client.NewGVR("test"), "fred/blee", "c1", false)
|
||||
l.Init(makeContext())
|
||||
buff := dao.LogItems{
|
||||
dao.NewLogItemFromString("duh"),
|
||||
dao.NewLogItemFromString("zorg"),
|
||||
}
|
||||
var list logList
|
||||
l.GetModel().AddListener(&list)
|
||||
l.GetModel().Set(buff)
|
||||
l.SendKeys(ui.KeySlash)
|
||||
l.SendStrokes("zorg")
|
||||
|
||||
assert.Equal(t, "zorg", list.lines)
|
||||
assert.Equal(t, 5, list.change)
|
||||
assert.Equal(t, 5, list.clear)
|
||||
assert.Equal(t, 0, list.fail)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
type logList struct {
|
||||
change, clear, fail int
|
||||
lines string
|
||||
}
|
||||
|
||||
func (l *logList) LogChanged(ii dao.LogItems) {
|
||||
l.change++
|
||||
l.lines = ""
|
||||
for _, i := range ii {
|
||||
l.lines += string(i.Render(false))
|
||||
}
|
||||
}
|
||||
func (l *logList) LogCleared() { l.clear++ }
|
||||
func (l *logList) LogFailed(error) { l.fail++ }
|
||||
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/derailed/k9s/internal/view"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -28,24 +29,12 @@ func TestLogAnsi(t *testing.T) {
|
|||
assert.Equal(t, s+"\n", v.GetText(false))
|
||||
}
|
||||
|
||||
func TestLogAutoScroll(t *testing.T) {
|
||||
v := view.NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
|
||||
v.Init(makeContext())
|
||||
v.GetModel().Set([]string{"blee", "bozo"})
|
||||
v.GetModel().Notify(true)
|
||||
|
||||
assert.Equal(t, 6, len(v.Hints()))
|
||||
|
||||
v.ToggleAutoScrollCmd(nil)
|
||||
assert.Equal(t, " Autoscroll: Off FullScreen: Off Wrap: Off ", v.Indicator().GetText(true))
|
||||
}
|
||||
|
||||
func TestLogViewSave(t *testing.T) {
|
||||
v := view.NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
|
||||
v.Init(makeContext())
|
||||
|
||||
app := makeApp()
|
||||
v.Flush([]string{"blee", "bozo"})
|
||||
v.Flush(dao.LogItems{dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo")})
|
||||
config.K9sDumpDir = "/tmp"
|
||||
dir := filepath.Join(config.K9sDumpDir, app.Config.K9s.CurrentCluster)
|
||||
c1, _ := ioutil.ReadDir(dir)
|
||||
|
|
@ -54,31 +43,6 @@ func TestLogViewSave(t *testing.T) {
|
|||
assert.Equal(t, len(c2), len(c1)+1)
|
||||
}
|
||||
|
||||
func TestLogViewNav(t *testing.T) {
|
||||
v := view.NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
|
||||
v.Init(makeContext())
|
||||
|
||||
var buff []string
|
||||
for i := 0; i < 100; i++ {
|
||||
buff = append(buff, fmt.Sprintf("line-%d\n", i))
|
||||
}
|
||||
v.GetModel().Set(buff)
|
||||
v.ToggleAutoScrollCmd(nil)
|
||||
|
||||
r, _ := v.Logs().GetScrollOffset()
|
||||
assert.Equal(t, 0, r)
|
||||
}
|
||||
|
||||
func TestLogViewClear(t *testing.T) {
|
||||
v := view.NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false)
|
||||
v.Init(makeContext())
|
||||
|
||||
v.ToggleAutoScrollCmd(nil)
|
||||
v.Logs().SetText("blee\nblah")
|
||||
v.Logs().Clear()
|
||||
assert.Equal(t, "", v.Logs().GetText(true))
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ import (
|
|||
|
||||
const portForwardKey = "portforward"
|
||||
|
||||
// PortForwardFunc represents a port-forward callback function.
|
||||
type PortForwardFunc func(v ResourceViewer, path, co string, mapper client.PortTunnel)
|
||||
// PortForwardCB represents a port-forward callback function.
|
||||
type PortForwardCB func(v ResourceViewer, path, co string, mapper client.PortTunnel)
|
||||
|
||||
// ShowPortForwards pops a port forwarding configuration dialog.
|
||||
func ShowPortForwards(v ResourceViewer, path string, ports []string, okFn PortForwardFunc) {
|
||||
func ShowPortForwards(v ResourceViewer, path string, ports []string, okFn PortForwardCB) {
|
||||
styles := v.App().Styles
|
||||
|
||||
f := tview.NewForm()
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ func startFwdCB(v ResourceViewer, path, co string, t client.PortTunnel) {
|
|||
go runForward(v, pf, fwd)
|
||||
}
|
||||
|
||||
func showFwdDialog(v ResourceViewer, path string, cb PortForwardFunc) error {
|
||||
func showFwdDialog(v ResourceViewer, path string, cb PortForwardCB) error {
|
||||
mm, err := fetchPodPorts(v.App().factory, path)
|
||||
if err != nil {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/atotto/clipboard"
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
|
@ -132,7 +133,7 @@ func (t *Table) SetExtraActionsFn(BoostActionsFunc) {}
|
|||
func (t *Table) BufferChanged(s string) {}
|
||||
|
||||
// BufferActive indicates the buff activity changed.
|
||||
func (t *Table) BufferActive(state bool, k ui.BufferKind) {
|
||||
func (t *Table) BufferActive(state bool, k model.BufferKind) {
|
||||
t.app.BufferActive(state, k)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ func TestTableViewFilter(t *testing.T) {
|
|||
v.SearchBuff().SetActive(true)
|
||||
v.SearchBuff().Set("blee")
|
||||
v.Refresh()
|
||||
assert.Equal(t, 2, v.GetRowCount())
|
||||
assert.Equal(t, 1, v.GetRowCount())
|
||||
}
|
||||
|
||||
func TestTableViewSort(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -576,7 +576,7 @@ func (x *Xray) Refresh() {
|
|||
func (x *Xray) BufferChanged(s string) {}
|
||||
|
||||
// BufferActive indicates the buff activity changed.
|
||||
func (x *Xray) BufferActive(state bool, k ui.BufferKind) {
|
||||
func (x *Xray) BufferActive(state bool, k model.BufferKind) {
|
||||
x.app.BufferActive(state, k)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ const (
|
|||
)
|
||||
|
||||
func colorizeYAML(style config.Yaml, raw string) string {
|
||||
// lines := strings.Split(raw, "\n")
|
||||
lines := strings.Split(tview.Escape(raw), "\n")
|
||||
|
||||
fullFmt := strings.Replace(yamlFullFmt, "[key", "["+style.KeyColor.String(), 1)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import (
|
|||
|
||||
const (
|
||||
defaultResync = 10 * time.Minute
|
||||
defaultWaitTime = 500 * time.Millisecond
|
||||
defaultWaitTime = 250 * time.Millisecond
|
||||
)
|
||||
|
||||
// Factory tracks various resource informers.
|
||||
|
|
|
|||
|
|
@ -68,6 +68,9 @@ k9s:
|
|||
logs:
|
||||
fgColor: *ghost
|
||||
bgColor: *bg
|
||||
indicator:
|
||||
fgColor: *ghost
|
||||
bgColor: *bg
|
||||
charts:
|
||||
bgColor: default
|
||||
defaultDialColors:
|
||||
|
|
|
|||
|
|
@ -90,3 +90,6 @@ k9s:
|
|||
logs:
|
||||
fgColor: *foreground
|
||||
bgColor: *background
|
||||
indicator:
|
||||
fgColor: *foreground
|
||||
bgColor: *purple
|
||||
|
|
|
|||
|
|
@ -84,3 +84,6 @@ k9s:
|
|||
logs:
|
||||
fgColor: *dark
|
||||
bgColor: *bg
|
||||
indicator:
|
||||
fgColor: *dark
|
||||
bgColor: *bg
|
||||
|
|
|
|||
|
|
@ -64,3 +64,6 @@ k9s:
|
|||
logs:
|
||||
fgColor: white
|
||||
bgColor: "#282a36"
|
||||
indicator:
|
||||
fgColor: white
|
||||
bgColor: "#282a36"
|
||||
|
|
|
|||
|
|
@ -63,3 +63,6 @@ k9s:
|
|||
logs:
|
||||
fgColor: white
|
||||
bgColor: black
|
||||
indicator:
|
||||
fgColor: dodgerblue
|
||||
bgColor: black
|
||||
|
|
|
|||
Loading…
Reference in New Issue