package view import ( "context" "fmt" "image" "strings" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/health" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/tchart" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" "github.com/gdamore/tcell" ) // Grapheable represents a graphic component. type Grapheable interface { tview.Primitive // ID returns the graph id. ID() string // Add adds a metric Add(tchart.Metric) // SetLegend sets the graph legend SetLegend(string) // SetSeriesColors sets charts series colors. SetSeriesColors(...tcell.Color) // GetSeriesColorNames returns the series color names. GetSeriesColorNames() []string // SetBackgroundColor sets chart bg color. SetBackgroundColor(tcell.Color) // IsDial returns true if chart is a dial IsDial() bool } const pulseTitle = "Pulses" var _ ResourceViewer = (*Pulse)(nil) // Pulse represents a command health view. type Pulse struct { *tview.Grid app *App gvr client.GVR model *model.Pulse cancelFn context.CancelFunc actions ui.KeyActions charts []Grapheable } // NewPulse returns a new alias view. func NewPulse(gvr client.GVR) ResourceViewer { return &Pulse{ Grid: tview.NewGrid(), model: model.NewPulse(gvr.String()), actions: make(ui.KeyActions), } } // Init initializes the view. func (p *Pulse) Init(ctx context.Context) error { p.SetBorder(true) p.SetTitle(fmt.Sprintf(" %s ", pulseTitle)) p.SetGap(1, 1) p.SetBorderPadding(0, 0, 1, 1) var err error if p.app, err = extractApp(ctx); err != nil { return err } p.charts = []Grapheable{ p.makeGA(image.Point{X: 0, Y: 0}, image.Point{X: 4, Y: 2}, "apps/v1/deployments"), p.makeGA(image.Point{X: 0, Y: 2}, image.Point{X: 4, Y: 2}, "apps/v1/replicasets"), p.makeGA(image.Point{X: 0, Y: 4}, image.Point{X: 4, Y: 2}, "apps/v1/statefulsets"), p.makeGA(image.Point{X: 0, Y: 6}, image.Point{X: 4, Y: 2}, "apps/v1/daemonsets"), p.makeSP(image.Point{X: 4, Y: 0}, image.Point{X: 3, Y: 4}, "v1/pods"), p.makeSP(image.Point{X: 4, Y: 4}, image.Point{X: 3, Y: 4}, "v1/events"), p.makeSP(image.Point{X: 7, Y: 0}, image.Point{X: 3, Y: 4}, "batch/v1/jobs"), p.makeSP(image.Point{X: 7, Y: 4}, image.Point{X: 3, Y: 4}, "v1/persistentvolumes"), } if p.app.Conn().HasMetrics() { p.charts = append(p.charts, p.makeSP(image.Point{X: 10, Y: 0}, image.Point{X: 2, Y: 4}, "cpu"), p.makeSP(image.Point{X: 10, Y: 4}, image.Point{X: 2, Y: 4}, "mem"), ) } p.bindKeys() p.model.AddListener(p) p.app.SetFocus(p.charts[0]) p.app.Styles.AddListener(p) return nil } // StylesChanged notifies the skin changed. func (p *Pulse) StylesChanged(s *config.Styles) { p.SetBackgroundColor(s.Charts().BgColor.Color()) for _, c := range p.charts { if c.IsDial() { c.SetBackgroundColor(s.Charts().DialBgColor.Color()) c.SetSeriesColors(s.Charts().DefaultDialColors.Colors()...) } else { c.SetBackgroundColor(s.Charts().ChartBgColor.Color()) c.SetSeriesColors(s.Charts().DefaultChartColors.Colors()...) } if ss, ok := s.Charts().ResourceColors[c.ID()]; ok { c.SetSeriesColors(ss.Colors()...) } } p.app.Draw() } // PulseChanged notifies the model data changed. func (p *Pulse) PulseChanged(c *health.Check) { index, ok := findIndexGVR(p.charts, c.GVR) if !ok { return } v, ok := p.GetItem(index).Item.(Grapheable) if !ok { return } gvr := client.NewGVR(c.GVR) switch c.GVR { case "cpu": v.SetLegend(fmt.Sprintf(" %s - %dm", strings.Title(gvr.R()), c.Tally(health.OK))) case "mem": v.SetLegend(fmt.Sprintf(" %s - %dMi", strings.Title(gvr.R()), c.Tally(health.OK))) default: nn := v.GetSeriesColorNames() if c.Tally(health.OK) == 0 { nn[0] = "gray" } if c.Tally(health.Toast) == 0 { nn[1] = "gray" } v.SetLegend(fmt.Sprintf(" %s - [%s::]%d/[%s::b]%d[-::]", strings.Title(gvr.R()), nn[0], c.Tally(health.OK), nn[1], c.Tally(health.Toast), )) } v.Add(tchart.Metric{OK: c.Tally(health.OK), Fault: c.Tally(health.Toast)}) } // PulseLoadFailed notifies the load failed. func (p *Pulse) PulseFailed(err error) { p.app.Flash().Err(err) } func (p *Pulse) bindKeys() { p.actions.Add(ui.KeyActions{ tcell.KeyEnter: ui.NewKeyAction("Goto", p.enterCmd, true), tcell.KeyTab: ui.NewKeyAction("Next", p.nextFocusCmd(1), true), tcell.KeyBacktab: ui.NewKeyAction("Prev", p.nextFocusCmd(-1), true), }) for i, v := range p.charts { t := strings.Title(client.NewGVR(v.(Grapheable).ID()).R()) p.actions[tcell.Key(ui.NumKeys[i])] = ui.NewKeyAction(t, p.sparkFocusCmd(i), true) } } func (p *Pulse) keyboard(evt *tcell.EventKey) *tcell.EventKey { key := evt.Key() if key == tcell.KeyRune { key = tcell.Key(evt.Rune()) } if a, ok := p.actions[key]; ok { return a.Action(evt) } return evt } func (p *Pulse) defaultContext() context.Context { return context.WithValue(context.Background(), internal.KeyFactory, p.app.factory) } // Start initializes resource watch loop. func (p *Pulse) Start() { p.Stop() ctx := p.defaultContext() ctx, p.cancelFn = context.WithCancel(ctx) p.model.Watch(ctx) } // Stop terminates watch loop. func (p *Pulse) Stop() { if p.cancelFn == nil { return } p.cancelFn() p.cancelFn = nil } // Refresh updates the view func (p *Pulse) Refresh() { // p.update(p.model.Peek()) } // GVR returns a resource descriptor. func (p *Pulse) GVR() string { return p.gvr.String() } // Name returns the component name. func (p *Pulse) Name() string { return pulseTitle } // App returns the current app handle. func (p *Pulse) App() *App { return p.app } // SetInstance sets specific resource instance. func (p *Pulse) SetInstance(string) {} // SetEnvFn sets the custom environment function. func (p *Pulse) SetEnvFn(EnvFunc) {} // SetBindKeysFn sets up extra key bindings. func (p *Pulse) SetBindKeysFn(BindKeysFunc) {} // SetContextFn sets custom context. func (p *Pulse) SetContextFn(ContextFunc) {} // GetTable return the view table if any. func (p *Pulse) GetTable() *Table { return nil } // Actions returns active menu bindings. func (p *Pulse) Actions() ui.KeyActions { return p.actions } // Hints returns the view hints. func (p *Pulse) Hints() model.MenuHints { return p.actions.Hints() } // ExtraHints returns additional hints. func (p *Pulse) ExtraHints() map[string]string { return nil } func (p *Pulse) sparkFocusCmd(i int) func(evt *tcell.EventKey) *tcell.EventKey { return func(evt *tcell.EventKey) *tcell.EventKey { p.app.SetFocus(p.charts[i]) return nil } } func (p *Pulse) enterCmd(evt *tcell.EventKey) *tcell.EventKey { v := p.App().GetFocus() s, ok := v.(Grapheable) if !ok { return nil } gvr := client.NewGVR(s.ID()) if err := p.App().gotoResource(gvr.R()+" all", "", false); err != nil { p.App().Flash().Err(err) } return nil } func (p *Pulse) nextFocusCmd(direction int) func(evt *tcell.EventKey) *tcell.EventKey { return func(evt *tcell.EventKey) *tcell.EventKey { v := p.app.GetFocus() index := findIndex(p.charts, v) p.GetItem(index).Focus = false p.GetItem(index).Item.Blur() i, v := nextFocus(p.charts, index+direction) p.GetItem(i).Focus = true p.app.SetFocus(v) return nil } } func (p *Pulse) makeSP(loc image.Point, span image.Point, gvr string) *tchart.SparkLine { s := tchart.NewSparkLine(gvr) s.SetBackgroundColor(p.app.Styles.Charts().BgColor.Color()) s.SetBorderPadding(0, 1, 0, 1) if cc, ok := p.app.Styles.Charts().ResourceColors[gvr]; ok { s.SetSeriesColors(cc.Colors()...) } else { s.SetSeriesColors(p.app.Styles.Charts().DefaultChartColors.Colors()...) } s.SetLegend(fmt.Sprintf(" %s ", strings.Title(client.NewGVR(gvr).R()))) s.SetInputCapture(p.keyboard) p.AddItem(s, loc.X, loc.Y, span.X, span.Y, 0, 0, true) return s } func (p *Pulse) makeGA(loc image.Point, span image.Point, gvr string) *tchart.Gauge { g := tchart.NewGauge(gvr) g.SetBackgroundColor(p.app.Styles.Charts().BgColor.Color()) g.SetBorderPadding(0, 1, 0, 1) if cc, ok := p.app.Styles.Charts().ResourceColors[gvr]; ok { g.SetSeriesColors(cc.Colors()...) } else { g.SetSeriesColors(p.app.Styles.Charts().DefaultDialColors.Colors()...) } g.SetLegend(fmt.Sprintf(" %s ", strings.Title(client.NewGVR(gvr).R()))) g.SetInputCapture(p.keyboard) p.AddItem(g, loc.X, loc.Y, span.X, span.Y, 0, 0, true) return g } // ---------------------------------------------------------------------------- // Helpers func nextFocus(pp []Grapheable, index int) (int, tview.Primitive) { if index >= len(pp) { return 0, pp[0] } if index < 0 { return len(pp) - 1, pp[len(pp)-1] } return index, pp[index] } func findIndex(pp []Grapheable, p tview.Primitive) int { for i, v := range pp { if v == p { return i } } return 0 } func findIndexGVR(pp []Grapheable, gvr string) (int, bool) { for i, v := range pp { if v.(Grapheable).ID() == gvr { return i, true } } return 0, false }