derailed 2020-02-27 21:47:21 -07:00
parent e936b17fd0
commit c896598e84
22 changed files with 385 additions and 154 deletions

View File

@ -0,0 +1,24 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# Release v0.17.1
## Notes
Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!
Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)
On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)
---
Maintenance Release!
## Resolved Bugs/Features/PRs
- [Issue #584](https://github.com/derailed/k9s/issues/584)
- [Issue #583](https://github.com/derailed/k9s/issues/583)
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -251,7 +251,7 @@ func newStatus() Status {
ErrorColor: "orangered", ErrorColor: "orangered",
HighlightColor: "aqua", HighlightColor: "aqua",
KillColor: "mediumpurple", KillColor: "mediumpurple",
CompletedColor: "darkseagreen", CompletedColor: "lightslategray",
} }
} }

View File

@ -16,6 +16,7 @@ type LogOptions struct {
Previous bool Previous bool
SingleContainer bool SingleContainer bool
MultiPods bool MultiPods bool
ShowTimestamp bool
} }
// HasContainer checks if a container is present. // HasContainer checks if a container is present.

View File

@ -227,6 +227,7 @@ func tailLogs(ctx context.Context, logger Logger, c chan<- []byte, opts LogOptio
Container: opts.Container, Container: opts.Container,
Follow: true, Follow: true,
TailLines: &opts.Lines, TailLines: &opts.Lines,
Timestamps: opts.ShowTimestamp,
Previous: opts.Previous, Previous: opts.Previous,
} }
req, err := logger.Logs(opts.Path, &o) req, err := logger.Logs(opts.Path, &o)

View File

@ -48,7 +48,7 @@ func (s *Service) Pod(fqn string) (string, error) {
// GetInstance returns a service instance. // GetInstance returns a service instance.
func (s *Service) GetInstance(fqn string) (*v1.Service, error) { func (s *Service) GetInstance(fqn string) (*v1.Service, error) {
o, err := s.Factory.Get(s.gvr.String(), fqn, false, labels.Everything()) o, err := s.Factory.Get(s.gvr.String(), fqn, true, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -25,6 +25,7 @@ const (
KeyApp ContextKey = "app" KeyApp ContextKey = "app"
KeyStyles ContextKey = "styles" KeyStyles ContextKey = "styles"
KeyMetrics ContextKey = "metrics" KeyMetrics ContextKey = "metrics"
KeyHasMetrics ContextKey = "has-metrics"
KeyToast ContextKey = "toast" KeyToast ContextKey = "toast"
KeyWithMetrics ContextKey = "withMetrics" KeyWithMetrics ContextKey = "withMetrics"
KeyViewConfig ContextKey = "viewConfig" KeyViewConfig ContextKey = "viewConfig"

View File

@ -15,7 +15,7 @@ import (
"github.com/sahilm/fuzzy" "github.com/sahilm/fuzzy"
) )
const logMaxBufferSize = 50 const logMaxBufferSize = 100
// LogsListener represents a log model listener. // LogsListener represents a log model listener.
type LogsListener interface { type LogsListener interface {
@ -40,6 +40,7 @@ type Log struct {
mx sync.RWMutex mx sync.RWMutex
filter string filter string
lastSent int lastSent int
showTimestamp bool
} }
// NewLog returns a new model. // NewLog returns a new model.
@ -72,6 +73,16 @@ func (l *Log) Clear() {
l.fireLogCleared() l.fireLogCleared()
} }
// ShowTimestamp toggles timestamp on logs.
func (l *Log) ShowTimestamp(b bool) {
l.mx.RLock()
defer l.mx.RUnlock()
l.showTimestamp = b
l.fireLogCleared()
l.fireLogChanged(l.lines)
}
// Start initialize log tailer. // Start initialize log tailer.
func (l *Log) Start() { func (l *Log) Start() {
if err := l.load(); err != nil { if err := l.load(); err != nil {
@ -100,6 +111,7 @@ func (l *Log) Set(lines []string) {
// ClearFilter resets the log filter if any. // ClearFilter resets the log filter if any.
func (l *Log) ClearFilter() { func (l *Log) ClearFilter() {
log.Debug().Msgf("CLEARED!!")
l.mx.RLock() l.mx.RLock()
defer l.mx.RUnlock() defer l.mx.RUnlock()
@ -112,6 +124,7 @@ func (l *Log) Filter(q string) error {
l.mx.RLock() l.mx.RLock()
defer l.mx.RUnlock() defer l.mx.RUnlock()
log.Debug().Msgf("FILTER!")
l.filter = q l.filter = q
filtered, err := applyFilter(l.filter, l.lines) filtered, err := applyFilter(l.filter, l.lines)
if err != nil { if err != nil {
@ -262,6 +275,7 @@ func applyFilter(q string, lines []string) ([]string, error) {
} }
func (l *Log) fireLogBuffChanged(lines []string) { func (l *Log) fireLogBuffChanged(lines []string) {
log.Debug().Msgf("FIRE-BUFF-CHNGED")
filtered, err := applyFilter(l.filter, lines) filtered, err := applyFilter(l.filter, lines)
if err != nil { if err != nil {
l.fireLogError(err) l.fireLogError(err)
@ -293,6 +307,22 @@ func (l *Log) fireLogCleared() {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...
// BOZO!! Log timestamps.
// func showTimes(lines []string, show bool) []string {
// filtered := make([]string, 0, len(lines))
// for _, l := range lines {
// tokens := strings.Split(l, " ")
// if show {
// cols := make([]string, 0, len(tokens))
// cols = append(cols, fmt.Sprintf("%-35s", tokens[0]))
// filtered = append(filtered, strings.Join(append(cols, tokens[1:]...), " "))
// } else {
// filtered = append(filtered, strings.Join(tokens[1:], " "))
// }
// }
// return filtered
// }
var fuzzyRx = regexp.MustCompile(`\A\-f`) var fuzzyRx = regexp.MustCompile(`\A\-f`)
func isFuzzySelector(s string) bool { func isFuzzySelector(s string) bool {

View File

@ -37,7 +37,6 @@ type Table struct {
refreshRate time.Duration refreshRate time.Duration
instance string instance string
mx sync.RWMutex mx sync.RWMutex
hasMetrics bool
} }
// NewTable returns a new table model. // NewTable returns a new table model.
@ -49,11 +48,6 @@ func NewTable(gvr client.GVR) *Table {
} }
} }
// HasMetrics determines if metrics are available on cluster.
func (t *Table) HasMetrics() bool {
return t.hasMetrics
}
// SetInstance sets a single entry table. // SetInstance sets a single entry table.
func (t *Table) SetInstance(path string) { func (t *Table) SetInstance(path string) {
t.instance = path t.instance = path
@ -221,7 +215,6 @@ func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, err
} }
a.Init(factory, t.gvr) a.Init(factory, t.gvr)
t.hasMetrics = factory.Client().HasMetrics()
ns := client.CleanseNamespace(t.namespace) ns := client.CleanseNamespace(t.namespace)
if client.IsClusterScoped(t.namespace) { if client.IsClusterScoped(t.namespace) {
ns = client.AllNamespaces ns = client.AllNamespaces
@ -275,6 +268,10 @@ func (t *Table) reconcile(ctx context.Context) error {
t.data.Update(rows) t.data.Update(rows)
t.data.SetHeader(t.namespace, meta.Renderer.Header(t.namespace)) t.data.SetHeader(t.namespace, meta.Renderer.Header(t.namespace))
if len(t.data.Header) == 0 {
return fmt.Errorf("fail to list resource %s", t.gvr)
}
return nil return nil
} }

View File

@ -1,6 +1,8 @@
package render package render
import "github.com/gdamore/tcell" import (
"github.com/gdamore/tcell"
)
var ( var (
// ModColor row modified color. // ModColor row modified color.

View File

@ -0,0 +1,63 @@
package render_test
import (
"testing"
"github.com/derailed/k9s/internal/render"
"github.com/gdamore/tcell"
"github.com/stretchr/testify/assert"
)
func TestDefaultColorer(t *testing.T) {
uu := map[string]struct {
re render.RowEvent
e tcell.Color
}{
"add": {
render.RowEvent{
Kind: render.EventAdd,
},
render.AddColor,
},
"update": {
render.RowEvent{
Kind: render.EventUpdate,
},
render.ModColor,
},
"delete": {
render.RowEvent{
Kind: render.EventDelete,
},
render.KillColor,
},
"no-change": {
render.RowEvent{
Kind: render.EventUnchanged,
},
render.StdColor,
},
"invalid": {
render.RowEvent{
Kind: render.EventUnchanged,
Row: render.Row{
Fields: render.Fields{"", "", "blah"},
},
},
render.ErrColor,
},
}
h := render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "VALID"},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, render.DefaultColorer("", h, u.re))
})
}
}

View File

@ -72,7 +72,6 @@ func (h Header) Customize(cols []string, wide bool) Header {
xx := make(map[int]struct{}, len(h)) xx := make(map[int]struct{}, len(h))
for _, c := range cols { for _, c := range cols {
idx := h.IndexOf(c, true) idx := h.IndexOf(c, true)
// BOZO!!
if idx == -1 { if idx == -1 {
log.Warn().Msgf("Column %s is not available on this resource", c) log.Warn().Msgf("Column %s is not available on this resource", c)
col := HeaderColumn{ col := HeaderColumn{

View File

@ -18,20 +18,20 @@ type Namespace struct{}
// ColorerFunc colors a resource row. // ColorerFunc colors a resource row.
func (n Namespace) ColorerFunc() ColorerFunc { func (n Namespace) ColorerFunc() ColorerFunc {
return func(ns string, h Header, re RowEvent) tcell.Color { return func(ns string, h Header, re RowEvent) tcell.Color {
if !Happy(ns, h, re.Row) { c := DefaultColorer(ns, h, re)
return ErrColor
}
if re.Kind == EventAdd {
return DefaultColorer(ns, h, re)
}
if re.Kind == EventUpdate { if re.Kind == EventUpdate {
return StdColor c = StdColor
} }
if strings.Contains(strings.TrimSpace(re.Row.Fields[0]), "*") { if strings.Contains(strings.TrimSpace(re.Row.Fields[0]), "*") {
return HighlightColor c = HighlightColor
} }
return DefaultColorer(ns, h, re) if !Happy(ns, h, re.Row) {
c = ErrColor
}
return c
} }
} }

View File

@ -4,38 +4,64 @@ import (
"testing" "testing"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/gdamore/tcell"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestNSColorer(t *testing.T) { func TestNSColorer(t *testing.T) {
var ( uu := map[string]struct {
ns = render.Row{Fields: render.Fields{"blee", "Active"}} re render.RowEvent
term = render.Row{Fields: render.Fields{"blee", render.Terminating}} e tcell.Color
dead = render.Row{Fields: render.Fields{"blee", "Inactive"}} }{
) "add": {
re: render.RowEvent{
uu := colorerUCs{ Kind: render.EventAdd,
// Add AllNS Row: render.Row{
{"", render.RowEvent{Kind: render.EventAdd, Row: ns}, render.AddColor}, Fields: render.Fields{
// Mod AllNS "blee",
{"", render.RowEvent{Kind: render.EventUpdate, Row: ns}, render.ModColor}, "Active",
// MoChange AllNS },
{"", render.RowEvent{Kind: render.EventUnchanged, Row: ns}, render.StdColor}, },
// Bust NS },
{"", render.RowEvent{Kind: render.EventUnchanged, Row: term}, render.ErrColor}, e: render.AddColor,
// Bust NS },
{"", render.RowEvent{Kind: render.EventUnchanged, Row: dead}, render.ErrColor}, "update": {
re: render.RowEvent{
Kind: render.EventUpdate,
Row: render.Row{
Fields: render.Fields{
"blee",
"Active",
},
},
},
e: render.StdColor,
},
"decorator": {
re: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{
Fields: render.Fields{
"blee*",
"Active",
},
},
},
e: render.HighlightColor,
},
} }
h := render.Header{ h := render.Header{
render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "NAME"},
render.HeaderColumn{Name: "B"}, render.HeaderColumn{Name: "STATUS"},
} }
var n render.Namespace var r render.Namespace
f := n.ColorerFunc() for k := range uu {
for _, u := range uu { u := uu[k]
assert.Equal(t, u.e, f(u.ns, h, u.r)) t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, r.ColorerFunc()("", h, u.re))
})
} }
} }

View File

@ -39,6 +39,9 @@ func (p Pod) ColorerFunc() ColorerFunc {
c = CompletedColor c = CompletedColor
case Running: case Running:
c = StdColor c = StdColor
if !Happy(ns, h, re.Row) {
c = ErrColor
}
case Terminating: case Terminating:
c = KillColor c = KillColor
default: default:
@ -46,7 +49,6 @@ func (p Pod) ColorerFunc() ColorerFunc {
c = ErrColor c = ErrColor
} }
} }
return c return c
} }
} }
@ -116,7 +118,7 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
} }
func (p Pod) diagnose(phase string, cr, ct int) error { func (p Pod) diagnose(phase string, cr, ct int) error {
if phase == "Completed" { if phase == Completed {
return nil return nil
} }
if cr != ct || ct == 0 { if cr != ct || ct == 0 {
@ -272,14 +274,14 @@ func (p *Pod) Phase(po *v1.Pod) string {
} }
status, ok = p.containerPhase(po.Status, status) status, ok = p.containerPhase(po.Status, status)
if ok && status == "Completed" { if ok && status == Completed {
status = "Running" status = Running
} }
if po.DeletionTimestamp == nil { if po.DeletionTimestamp == nil {
return status return status
} }
return "Terminating" return Terminating
} }
func (*Pod) containerPhase(st v1.PodStatus, status string) (string, bool) { func (*Pod) containerPhase(st v1.PodStatus, status string) (string, bool) {

View File

@ -12,58 +12,138 @@ import (
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
) )
type ( func init() {
colorerUC struct { render.AddColor = tcell.ColorBlue
ns string render.HighlightColor = tcell.ColorYellow
r render.RowEvent render.CompletedColor = tcell.ColorGray
e tcell.Color render.StdColor = tcell.ColorWhite
render.ErrColor = tcell.ColorRed
render.KillColor = tcell.ColorGray
} }
colorerUCs []colorerUC
)
func TestPodColorer(t *testing.T) { func TestPodColorer(t *testing.T) {
var ( stdHeader := render.Header{
nsRow = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "0", "Running"}} render.HeaderColumn{Name: "NAMESPACE"},
toastNS = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "0", "Boom"}} render.HeaderColumn{Name: "NAME"},
notReadyNS = render.Row{Fields: render.Fields{"blee", "fred", "0/1", "0", "Boom"}} render.HeaderColumn{Name: "READY"},
row = render.Row{Fields: render.Fields{"fred", "1/1", "0", "Running"}} render.HeaderColumn{Name: "RESTART"},
toast = render.Row{Fields: render.Fields{"fred", "1/1", "0", "Boom"}} render.HeaderColumn{Name: "STATUS"},
notReady = render.Row{Fields: render.Fields{"fred", "0/1", "0", "Boom"}} render.HeaderColumn{Name: "VALID"},
)
uu := colorerUCs{
// Add allNS
{"", render.RowEvent{Kind: render.EventAdd, Row: nsRow}, render.AddColor},
// Add Namespaced
{"blee", render.RowEvent{Kind: render.EventAdd, Row: row}, render.AddColor},
// Mod AllNS
{"", render.RowEvent{Kind: render.EventUpdate, Row: nsRow}, render.ModColor},
// Mod Namespaced
{"blee", render.RowEvent{Kind: render.EventUpdate, Row: row}, render.ModColor},
// Mod Busted AllNS
{"", render.RowEvent{Kind: render.EventUpdate, Row: toastNS}, render.ErrColor},
// Mod Busted Namespaced
{"blee", render.RowEvent{Kind: render.EventUpdate, Row: toast}, render.ErrColor},
// NotReady AllNS
{"", render.RowEvent{Kind: render.EventUpdate, Row: notReadyNS}, render.ErrColor},
// NotReady Namespaced
{"blee", render.RowEvent{Kind: render.EventUpdate, Row: notReady}, render.ErrColor},
} }
h := render.Header{ uu := map[string]struct {
render.HeaderColumn{Name: "A"}, re render.RowEvent
render.HeaderColumn{Name: "B"}, h render.Header
render.HeaderColumn{Name: "C"}, e tcell.Color
render.HeaderColumn{Name: "D"}, }{
render.HeaderColumn{Name: "E"}, "valid": {
render.HeaderColumn{Name: "F"}, h: stdHeader,
re: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", render.Running, ""},
},
},
e: render.StdColor,
},
"init": {
h: stdHeader,
re: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", render.PodInitializing, ""},
},
},
e: render.AddColor,
},
"init-err": {
h: stdHeader,
re: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", render.PodInitializing, "blah"},
},
},
e: render.AddColor,
},
"initialized": {
h: stdHeader,
re: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", render.Initialized, "blah"},
},
},
e: render.HighlightColor,
},
"completed": {
h: stdHeader,
re: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", render.Completed, "blah"},
},
},
e: render.CompletedColor,
},
"terminating": {
h: stdHeader,
re: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", render.Terminating, "blah"},
},
},
e: render.KillColor,
},
"invalid": {
h: stdHeader,
re: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", "Running", "blah"},
},
},
e: render.ErrColor,
},
"unknown-cool": {
h: stdHeader,
re: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", "blee", ""},
},
},
e: render.AddColor,
},
"unknown-err": {
h: stdHeader,
re: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", "blee", "doh"},
},
},
e: render.ErrColor,
},
"status": {
h: stdHeader[0:3],
re: render.RowEvent{
Kind: render.EventDelete,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", "blee", ""},
},
},
e: render.KillColor,
},
} }
var p render.Pod var r render.Pod
f := p.ColorerFunc() for k := range uu {
for _, u := range uu { u := uu[k]
assert.Equal(t, u.e, f(u.ns, h, u.r)) t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, r.ColorerFunc()("", u.h, u.re))
})
} }
} }

View File

@ -4,7 +4,6 @@ import (
"testing" "testing"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/gdamore/tcell"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -485,31 +484,6 @@ func TestRowEventsClone(t *testing.T) {
} }
} }
func TestDefaultColorer(t *testing.T) {
uu := map[string]struct {
k render.ResEvent
e tcell.Color
}{
"add": {render.EventAdd, render.AddColor},
"update": {render.EventUpdate, render.ModColor},
"delete": {render.EventDelete, render.KillColor},
"std": {100, render.StdColor},
}
h := render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, render.DefaultColorer("", h, render.RowEvent{}))
})
}
}
// Helpers... // Helpers...
func makeRowEvents() render.RowEvents { func makeRowEvents() render.RowEvents {

View File

@ -43,6 +43,7 @@ type Table struct {
wide bool wide bool
toast bool toast bool
header render.Header header render.Header
hasMetrics bool
} }
// NewTable returns a new table view. // NewTable returns a new table view.
@ -71,6 +72,11 @@ func (t *Table) Init(ctx context.Context) {
t.SetSelectionChangedFunc(t.selectionChanged) t.SetSelectionChangedFunc(t.selectionChanged)
t.SetBackgroundColor(tcell.ColorDefault) t.SetBackgroundColor(tcell.ColorDefault)
t.hasMetrics = false
if mx, ok := ctx.Value(internal.KeyHasMetrics).(bool); ok {
t.hasMetrics = mx
}
if cfg, ok := ctx.Value(internal.KeyViewConfig).(*config.CustomView); ok && cfg != nil { if cfg, ok := ctx.Value(internal.KeyViewConfig).(*config.CustomView); ok && cfg != nil {
cfg.AddListener(t.GVR().String(), t) cfg.AddListener(t.GVR().String(), t)
} }
@ -198,7 +204,7 @@ func (t *Table) doUpdate(data render.TableData) {
} }
custData := data.Customize(cols, t.wide) custData := data.Customize(cols, t.wide)
if t.sortCol.name == "" || custData.Header.IndexOf(t.sortCol.name, false) == -1 { if (t.sortCol.name == "" || custData.Header.IndexOf(t.sortCol.name, false) == -1) && len(custData.Header) > 0 {
t.sortCol.name = custData.Header[0].Name t.sortCol.name = custData.Header[0].Name
} }
@ -206,13 +212,12 @@ func (t *Table) doUpdate(data render.TableData) {
fg := t.styles.Table().Header.FgColor.Color() fg := t.styles.Table().Header.FgColor.Color()
bg := t.styles.Table().Header.BgColor.Color() bg := t.styles.Table().Header.BgColor.Color()
hasMX := t.model.HasMetrics()
var col int var col int
for _, h := range custData.Header { for _, h := range custData.Header {
if h.Name == "NAMESPACE" && !t.GetModel().ClusterWide() { if h.Name == "NAMESPACE" && !t.GetModel().ClusterWide() {
continue continue
} }
if h.MX && !hasMX { if h.MX && !t.hasMetrics {
continue continue
} }
t.AddHeaderCell(col, h) t.AddHeaderCell(col, h)
@ -239,7 +244,6 @@ func (t *Table) buildRow(r int, re, ore render.RowEvent, h render.Header, pads M
} }
marked := t.IsMarked(re.Row.ID) marked := t.IsMarked(re.Row.ID)
hasMX := t.model.HasMetrics()
var col int var col int
for c, field := range re.Row.Fields { for c, field := range re.Row.Fields {
if c >= len(h) { if c >= len(h) {
@ -250,7 +254,7 @@ func (t *Table) buildRow(r int, re, ore render.RowEvent, h render.Header, pads M
if h[c].Name == "NAMESPACE" && !t.GetModel().ClusterWide() { if h[c].Name == "NAMESPACE" && !t.GetModel().ClusterWide() {
continue continue
} }
if h[c].MX && !hasMX { if h[c].MX && !t.hasMetrics {
continue continue
} }

View File

@ -52,6 +52,7 @@ type Tabular interface {
Namespaceable Namespaceable
Lister Lister
// SetInstance sets parent resource path.
SetInstance(string) SetInstance(string)
// Empty returns true if model has no data. // Empty returns true if model has no data.
@ -60,9 +61,6 @@ type Tabular interface {
// Peek returns current model data. // Peek returns current model data.
Peek() render.TableData Peek() render.TableData
// HasMetrics returns true if metrics are available on cluster.
HasMetrics() bool
// Watch watches a given resource for changes. // Watch watches a given resource for changes.
Watch(context.Context) Watch(context.Context)

View File

@ -26,7 +26,7 @@ const (
logFmt = " Logs([fg:bg:]%s) " logFmt = " Logs([fg:bg:]%s) "
// BOZO!! Canned! Need config tail line counts! // BOZO!! Canned! Need config tail line counts!
tailLineCount = 1000 tailLineCount = 50
defaultTimeout = 200 * time.Millisecond defaultTimeout = 200 * time.Millisecond
) )
@ -49,7 +49,7 @@ func NewLog(gvr client.GVR, path, co string, prev bool) *Log {
l := Log{ l := Log{
Flex: tview.NewFlex(), Flex: tview.NewFlex(),
cmdBuff: ui.NewCmdBuff('/', ui.FilterBuff), cmdBuff: ui.NewCmdBuff('/', ui.FilterBuff),
model: model.NewLog(gvr, buildLogOpts(path, co, prev, tailLineCount), defaultTimeout), model: model.NewLog(gvr, buildLogOpts(path, co, prev, true, tailLineCount), defaultTimeout),
} }
return &l return &l
@ -171,6 +171,8 @@ func (l *Log) bindKeys() {
tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, true), tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, true),
ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true), ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true),
ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.ToggleAutoScrollCmd, true), ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.ToggleAutoScrollCmd, true),
// BOZO!! Log timestamps
// ui.KeyT: ui.NewKeyAction("Toggle Timestamp", l.ToggleTimestampCmd, true),
ui.KeyF: ui.NewKeyAction("FullScreen", l.fullScreenCmd, true), ui.KeyF: ui.NewKeyAction("FullScreen", l.fullScreenCmd, true),
ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.textWrapCmd, true), ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.textWrapCmd, true),
tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true), tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true),
@ -353,6 +355,17 @@ func (l *Log) textWrapCmd(*tcell.EventKey) *tcell.EventKey {
return nil return nil
} }
// ToggleTimeStampCmd toggles timestamp field.
func (l *Log) ToggleTimestampCmd(evt *tcell.EventKey) *tcell.EventKey {
l.model.Clear()
l.indicator.ToggleTimestamp()
l.model.ShowTimestamp(l.indicator.Timestamp())
l.model.Stop()
l.model.Start()
return nil
}
// ToggleAutoScrollCmd toggles autoscroll status. // ToggleAutoScrollCmd toggles autoscroll status.
func (l *Log) ToggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey { func (l *Log) ToggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
l.indicator.ToggleAutoScroll() l.indicator.ToggleAutoScroll()
@ -392,11 +405,12 @@ func extractKey(evt *tcell.EventKey) tcell.Key {
return key return key
} }
func buildLogOpts(path, co string, prevLogs bool, tailLineCount int) dao.LogOptions { func buildLogOpts(path, co string, prevLogs, showTime bool, tailLineCount int) dao.LogOptions {
return dao.LogOptions{ return dao.LogOptions{
Path: path, Path: path,
Container: co, Container: co,
Lines: int64(tailLineCount), Lines: int64(tailLineCount),
Previous: prevLogs, Previous: prevLogs,
ShowTimestamp: showTime,
} }
} }

View File

@ -16,6 +16,7 @@ type LogIndicator struct {
scrollStatus int32 scrollStatus int32
fullScreen bool fullScreen bool
textWrap bool textWrap bool
showTime bool
} }
// NewLogIndicator returns a new indicator. // NewLogIndicator returns a new indicator.
@ -38,6 +39,11 @@ func (l *LogIndicator) AutoScroll() bool {
return atomic.LoadInt32(&l.scrollStatus) == 1 return atomic.LoadInt32(&l.scrollStatus) == 1
} }
// TextWrap reports the current wrap mode.
func (l *LogIndicator) Timestamp() bool {
return l.showTime
}
// TextWrap reports the current wrap mode. // TextWrap reports the current wrap mode.
func (l *LogIndicator) TextWrap() bool { func (l *LogIndicator) TextWrap() bool {
return l.textWrap return l.textWrap
@ -48,6 +54,11 @@ func (l *LogIndicator) FullScreen() bool {
return l.fullScreen return l.fullScreen
} }
// TextWrap reports the current wrap mode.
func (l *LogIndicator) ToggleTimestamp() {
l.showTime = !l.showTime
}
// ToggleFullScreen toggles the screen mode. // ToggleFullScreen toggles the screen mode.
func (l *LogIndicator) ToggleFullScreen() { func (l *LogIndicator) ToggleFullScreen() {
l.fullScreen = !l.fullScreen l.fullScreen = !l.fullScreen
@ -75,6 +86,8 @@ func (l *LogIndicator) Refresh() {
l.Clear() l.Clear()
l.update("Autoscroll: " + l.onOff(l.AutoScroll())) l.update("Autoscroll: " + l.onOff(l.AutoScroll()))
l.update("FullScreen: " + l.onOff(l.fullScreen)) l.update("FullScreen: " + l.onOff(l.fullScreen))
// BOZO!! log timestamp
// l.update("Timestamp: " + l.onOff(l.showTime))
l.update("Wrap: " + l.onOff(l.textWrap)) l.update("Wrap: " + l.onOff(l.textWrap))
} }

View File

@ -53,7 +53,6 @@ func isResourcePath(p string) bool {
} }
func (l *LogsExtender) showLogs(path string, prev bool) { func (l *LogsExtender) showLogs(path string, prev bool) {
// Need to load and wait for pods
ns, _ := client.Namespaced(path) ns, _ := client.Namespaced(path)
_, err := l.App().factory.CanForResource(ns, "v1/pods", client.MonitorAccess) _, err := l.App().factory.CanForResource(ns, "v1/pods", client.MonitorAccess)
if err != nil { if err != nil {

View File

@ -37,6 +37,9 @@ func (t *Table) Init(ctx context.Context) (err error) {
if t.app, err = extractApp(ctx); err != nil { if t.app, err = extractApp(ctx); err != nil {
return err return err
} }
if t.app.Conn() != nil {
ctx = context.WithValue(ctx, internal.KeyHasMetrics, t.app.Conn().HasMetrics())
}
ctx = context.WithValue(ctx, internal.KeyStyles, t.app.Styles) ctx = context.WithValue(ctx, internal.KeyStyles, t.app.Styles)
ctx = context.WithValue(ctx, internal.KeyViewConfig, t.app.CustomView) ctx = context.WithValue(ctx, internal.KeyViewConfig, t.app.CustomView)
t.Table.Init(ctx) t.Table.Init(ctx)