checkpoint
parent
9b15803117
commit
7a89c4c1e4
|
|
@ -26,7 +26,7 @@ Also if you dig this tool, please make some noise on social! [@kitesurfer](https
|
|||
|
||||
### o YAML Highlighter
|
||||
|
||||
Describe and YAML commands will now yield syntax highlighted views.
|
||||
Describe and YAML commands will now yield syntax highlighted view.
|
||||
[Feature #142](https://github.com/derailed/k9s/issues/142)
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ This is a maintenance release to mainly resolve outstanding issues and bugs.
|
|||
|
||||
### Tracking Percentages
|
||||
|
||||
Added two new columns to track cpu/memory utilization on metrics-server enabled clusters. These apply to pod,container and node views.
|
||||
Added two new columns to track cpu/memory utilization on metrics-server enabled clusters. These apply to pod,container and node view.
|
||||
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ This feature is still work in progress. It does require a new config file to hel
|
|||
|
||||
This is one feature that I think is, pardon my french.., totally `Bitch'n`! K9s now bundles [Hey](https://github.com/rakyll/hey) CLI tool from the extremely talented Jaana Dogan of Google fame. Hey allows you to benchmark HTTP service endpoints similar to apache bench.
|
||||
|
||||
Benchmarking is enabled via keyboard shortcuts `Ctrl-B` and `Alt-B` to activate/cancel a benchmark while in PortForward and Service views. Benchmarking a service assumes the HTTP service is exposed either as NodePort or LoadBalancer. To view your benchmarks, navigate to the new Benchmark view aka `:be<ENTER>` to list your benchmarks and runs statistics.
|
||||
Benchmarking is enabled via keyboard shortcuts `Ctrl-B` and `Alt-B` to activate/cancel a benchmark while in PortForward and Service view. Benchmarking a service assumes the HTTP service is exposed either as NodePort or LoadBalancer. To view your benchmarks, navigate to the new Benchmark view aka `:be<ENTER>` to list your benchmarks and runs statistics.
|
||||
|
||||
So you now have the ability to stretch out your cluster legs by benchmarking your pods and services and gather all kind of interesting statistics directly in K9s. Generating load on your resources will help you tune your cluster resources, exercise your auto scalers, compare Canary builds perf, etc...
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ dialogs. This was totally a reasonable thing to do! However in case of managed p
|
|||
|
||||
This one is cool! I think this thought came about from (Markus)[https://github.com/Makusi75]. Thank you Markus!! This feature allows K9s users to now customize K9s with their own plugin commands. You will be able to add a new menu shortcut to the K9s menu and fire off a custom command on a selected resource. Some of you might be leveraging kubectl plugins and now you will be able to fire these off directly from K9s along with many other shell commands.
|
||||
|
||||
In order to specify a custom plugin command, you will need to modify your .k9s/config.yml file. For example here is a sample extension to list out all the pods in the `fred` namespace while in a pod or deployment views. When this plugin is available a new command `<alt-p>` will show only while in pod and deploy views.
|
||||
In order to specify a custom plugin command, you will need to modify your .k9s/config.yml file. For example here is a sample extension to list out all the pods in the `fred` namespace while in a pod or deployment view. When this plugin is available a new command `<alt-p>` will show only while in pod and deploy view.
|
||||
|
||||
```yaml
|
||||
plugins:
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ Also if you dig this tool, please make some noise on social! [@kitesurfer](https
|
|||
|
||||
### NetworkPolicy
|
||||
|
||||
NetworkPolicy resource is now available under the command `np` while in command mode. It behaves like other K9s resource views. You will get a bit more information in K9s vs `kubectl` as it includes information about ingress and egress rules.
|
||||
NetworkPolicy resource is now available under the command `np` while in command mode. It behaves like other K9s resource view. You will get a bit more information in K9s vs `kubectl` as it includes information about ingress and egress rules.
|
||||
|
||||
### Arrrggg! New CLI Argument
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/views"
|
||||
"github.com/derailed/k9s/internal/view"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -59,7 +59,7 @@ func Execute() {
|
|||
|
||||
func run(cmd *cobra.Command, args []string) {
|
||||
defer func() {
|
||||
// views.ClearScreen()
|
||||
// view.ClearScreen()
|
||||
if err := recover(); err != nil {
|
||||
log.Error().Msgf("Boom! %v", err)
|
||||
log.Error().Msg(string(debug.Stack()))
|
||||
|
|
@ -71,7 +71,7 @@ func run(cmd *cobra.Command, args []string) {
|
|||
|
||||
zerolog.SetGlobalLevel(parseLevel(*k9sFlags.LogLevel))
|
||||
cfg := loadConfiguration()
|
||||
app := views.NewApp(cfg)
|
||||
app := view.NewApp(cfg)
|
||||
{
|
||||
defer app.BailOut()
|
||||
app.Init(version, *k9sFlags.RefreshRate)
|
||||
|
|
|
|||
1
go.sum
1
go.sum
|
|
@ -218,6 +218,7 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
|
|||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
benchmarks:
|
||||
defaults:
|
||||
concurrency: 2
|
||||
requests: 1000
|
||||
services:
|
||||
default/nginx:
|
||||
concurrency: 2
|
||||
requests: 1000
|
||||
http:
|
||||
method: GET
|
||||
http2: true
|
||||
host: 10.10.10.10
|
||||
path: /
|
||||
body: |-
|
||||
{"fred": "blee"}
|
||||
headers:
|
||||
Accept:
|
||||
- text/html
|
||||
Content-Type:
|
||||
- application/json
|
||||
auth:
|
||||
user: "fred"
|
||||
password: "blee"
|
||||
blee/fred:
|
||||
concurrency: 10
|
||||
requests: 1500
|
||||
http:
|
||||
method: POST
|
||||
http2: false
|
||||
host: 20.20.20.20
|
||||
path: /zorg
|
||||
body: |-
|
||||
{"fred": "blee"}
|
||||
headers:
|
||||
Accept:
|
||||
- text/html
|
||||
Content-Type:
|
||||
- application/json
|
||||
auth:
|
||||
user: "fred"
|
||||
password: "blee"
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package model
|
||||
|
||||
// HintListener represents a menu hints listener.
|
||||
type HintListener interface {
|
||||
HintsChanged(MenuHints)
|
||||
}
|
||||
|
||||
// Hint represent a hint model.
|
||||
type Hint struct {
|
||||
data MenuHints
|
||||
listeners []HintListener
|
||||
}
|
||||
|
||||
// NewHint return new hint model.
|
||||
func NewHint() *Hint {
|
||||
return &Hint{}
|
||||
}
|
||||
|
||||
// RemoveListener deletes a listener.
|
||||
func (h *Hint) RemoveListener(l HintListener) {
|
||||
victim := -1
|
||||
for i, lis := range h.listeners {
|
||||
if lis == l {
|
||||
victim = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if victim == -1 {
|
||||
return
|
||||
}
|
||||
h.listeners = append(h.listeners[:victim], h.listeners[victim+1:]...)
|
||||
}
|
||||
|
||||
// AddListener adds a hint listener.
|
||||
func (h *Hint) AddListener(l HintListener) {
|
||||
h.listeners = append(h.listeners, l)
|
||||
}
|
||||
|
||||
// SetHints set model hints.
|
||||
func (h *Hint) SetHints(hh MenuHints) {
|
||||
h.data = hh
|
||||
h.fireChanged()
|
||||
}
|
||||
|
||||
// Peek returns the model data.
|
||||
func (h *Hint) Peek() MenuHints {
|
||||
return h.data
|
||||
}
|
||||
|
||||
func (h *Hint) fireChanged() {
|
||||
for _, l := range h.listeners {
|
||||
l.HintsChanged(h.data)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package model_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHints(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
hh model.MenuHints
|
||||
e int
|
||||
}{
|
||||
"none": {
|
||||
model.MenuHints{},
|
||||
0,
|
||||
},
|
||||
"hints": {
|
||||
model.MenuHints{
|
||||
{Mnemonic: "a", Description: "blee"},
|
||||
{Mnemonic: "b", Description: "fred"},
|
||||
},
|
||||
2,
|
||||
},
|
||||
}
|
||||
|
||||
for k, u := range uu {
|
||||
t.Run(k, func(t *testing.T) {
|
||||
h := model.NewHint()
|
||||
l := hintL{count: -1}
|
||||
h.AddListener(&l)
|
||||
h.SetHints(u.hh)
|
||||
|
||||
assert.Equal(t, u.e, l.count)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHintRemoveListener(t *testing.T) {
|
||||
h := model.NewHint()
|
||||
l1, l2, l3 := &hintL{}, &hintL{}, &hintL{}
|
||||
h.AddListener(l1)
|
||||
h.AddListener(l2)
|
||||
h.AddListener(l3)
|
||||
|
||||
h.RemoveListener(l2)
|
||||
h.RemoveListener(l3)
|
||||
h.RemoveListener(l1)
|
||||
|
||||
h.SetHints(model.MenuHints{
|
||||
model.MenuHint{Mnemonic: "a", Description: "Blee"},
|
||||
})
|
||||
|
||||
assert.Equal(t, 0, l1.count)
|
||||
assert.Equal(t, 0, l2.count)
|
||||
assert.Equal(t, 0, l3.count)
|
||||
}
|
||||
|
||||
type hintL struct {
|
||||
count int
|
||||
}
|
||||
|
||||
func (h *hintL) HintsChanged(hh model.MenuHints) {
|
||||
h.count++
|
||||
h.count += len(hh)
|
||||
}
|
||||
|
|
@ -1,35 +1,29 @@
|
|||
package ui
|
||||
package model
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
// Hint represents keyboard mnemonic.
|
||||
Hint struct {
|
||||
Mnemonic string
|
||||
Description string
|
||||
Visible bool
|
||||
}
|
||||
// Hints a collection of keyboard mnemonics.
|
||||
Hints []Hint
|
||||
// MenuHint represents keyboard mnemonic.
|
||||
type MenuHint struct {
|
||||
Mnemonic string
|
||||
Description string
|
||||
Visible bool
|
||||
}
|
||||
|
||||
// Hinter returns a collection of mnemonics.
|
||||
Hinter interface {
|
||||
Hints() Hints
|
||||
}
|
||||
)
|
||||
// MenuHints represents a collection of hints.
|
||||
type MenuHints []MenuHint
|
||||
|
||||
func (h Hints) Len() int {
|
||||
func (h MenuHints) Len() int {
|
||||
return len(h)
|
||||
}
|
||||
|
||||
func (h Hints) Swap(i, j int) {
|
||||
func (h MenuHints) Swap(i, j int) {
|
||||
h[i], h[j] = h[j], h[i]
|
||||
}
|
||||
|
||||
func (h Hints) Less(i, j int) bool {
|
||||
func (h MenuHints) Less(i, j int) bool {
|
||||
n, err1 := strconv.Atoi(h[i].Mnemonic)
|
||||
m, err2 := strconv.Atoi(h[j].Mnemonic)
|
||||
if err1 == nil && err2 == nil {
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package model_test
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMenuHintOrder(t *testing.T) {
|
||||
h1 := model.MenuHint{Mnemonic: "b", Description: "Duh"}
|
||||
h2 := model.MenuHint{Mnemonic: "a", Description: "Blee"}
|
||||
h3 := model.MenuHint{Mnemonic: "1", Description: "Zorg"}
|
||||
|
||||
hh := model.MenuHints{h1, h2, h3}
|
||||
sort.Sort(hh)
|
||||
|
||||
assert.Equal(t, h3, hh[0])
|
||||
assert.Equal(t, h2, hh[1])
|
||||
assert.Equal(t, h1, hh[2])
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// StackPush denotes an add on the stack.
|
||||
StackPush StackAction = 1 << iota
|
||||
|
||||
// StackPop denotes a delete on the stack.
|
||||
StackPop
|
||||
)
|
||||
|
||||
// StackAction represents an action on the stack.
|
||||
type StackAction int
|
||||
|
||||
// StackEvent represents an operation on a view stack.
|
||||
type StackEvent struct {
|
||||
// Kind represents the event condition.
|
||||
Action StackAction
|
||||
|
||||
// Item represents the targetted item.
|
||||
Component Component
|
||||
}
|
||||
|
||||
// StackListener represents a stack listener.
|
||||
type StackListener interface {
|
||||
// StackPushed indicates a new item was added.
|
||||
StackPushed(Component)
|
||||
|
||||
// StackPopped indicates an item was deleted
|
||||
StackPopped(old, new Component)
|
||||
|
||||
// StackTop indicates the top of the stack
|
||||
StackTop(Component)
|
||||
}
|
||||
|
||||
// Stack represents a stacks of items.
|
||||
type Stack struct {
|
||||
components []Component
|
||||
listeners []StackListener
|
||||
}
|
||||
|
||||
// NewStack returns a new initialized stack.
|
||||
func NewStack() *Stack {
|
||||
return &Stack{}
|
||||
}
|
||||
|
||||
// Flatten retuns a string representation of the component stack.
|
||||
func (s *Stack) Flatten() []string {
|
||||
ss := make([]string, len(s.components))
|
||||
for i, c := range s.components {
|
||||
ss[i] = c.Name()
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
// RemoveListener removes a listener.
|
||||
func (s *Stack) RemoveListener(l StackListener) {
|
||||
victim := -1
|
||||
for i, lis := range s.listeners {
|
||||
if lis == l {
|
||||
victim = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if victim == -1 {
|
||||
return
|
||||
}
|
||||
s.listeners = append(s.listeners[:victim], s.listeners[victim+1:]...)
|
||||
}
|
||||
|
||||
// AddListener registers a stack listener.
|
||||
func (s *Stack) AddListener(l StackListener) {
|
||||
s.listeners = append(s.listeners, l)
|
||||
log.Debug().Msgf("Stack Add listener %#v", s.components)
|
||||
s.DumpStack()
|
||||
if s.Empty() {
|
||||
log.Debug().Msgf("Stack is empty!")
|
||||
} else {
|
||||
log.Debug().Msgf("TOP is %s", s.Top().Name())
|
||||
}
|
||||
l.StackTop(s.Top())
|
||||
}
|
||||
|
||||
// Dump prints out the stack.
|
||||
func (s *Stack) DumpStack() {
|
||||
log.Debug().Msgf("--- Stack Dump %p---", s)
|
||||
for i, c := range s.components {
|
||||
log.Debug().Msgf("%d -- %s -- %#v", i, c.Name(), c)
|
||||
}
|
||||
log.Debug().Msg("------------------")
|
||||
}
|
||||
|
||||
// Push adds a new item.
|
||||
func (s *Stack) Push(c Component) {
|
||||
if top := s.Top(); top != nil {
|
||||
top.Stop()
|
||||
}
|
||||
s.components = append(s.components, c)
|
||||
s.notify(StackPush, c)
|
||||
}
|
||||
|
||||
// Pop removed the top item and returns it.
|
||||
func (s *Stack) Pop() (Component, bool) {
|
||||
if s.Empty() {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
c := s.components[s.size()]
|
||||
s.components = s.components[:s.size()]
|
||||
s.notify(StackPop, c)
|
||||
c.Stop()
|
||||
|
||||
if top := s.Top(); top != nil {
|
||||
log.Debug().Msgf("Calling Restart on %s", top.Name())
|
||||
top.Start()
|
||||
}
|
||||
|
||||
return c, true
|
||||
}
|
||||
|
||||
// Empty returns true if the stack is empty.
|
||||
func (s *Stack) Empty() bool {
|
||||
return len(s.components) == 0
|
||||
}
|
||||
|
||||
// IsLast indicates if stack only has one item left.
|
||||
func (s *Stack) IsLast() bool {
|
||||
return len(s.components) == 1
|
||||
}
|
||||
|
||||
// Top returns the top most item or nil if the stack is empty.
|
||||
func (s *Stack) Top() Component {
|
||||
if s.Empty() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.components[s.size()]
|
||||
}
|
||||
|
||||
func (s *Stack) size() int {
|
||||
return len(s.components) - 1
|
||||
}
|
||||
|
||||
func (s *Stack) notify(a StackAction, c Component) {
|
||||
for _, l := range s.listeners {
|
||||
switch a {
|
||||
case StackPush:
|
||||
l.StackPushed(c)
|
||||
case StackPop:
|
||||
l.StackPopped(c, s.Top())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
package model_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zerolog.SetGlobalLevel(zerolog.FatalLevel)
|
||||
}
|
||||
|
||||
func TestStackPush(t *testing.T) {
|
||||
top := c{}
|
||||
uu := map[string]struct {
|
||||
items []model.Component
|
||||
pop int
|
||||
e bool
|
||||
top model.Component
|
||||
}{
|
||||
"empty": {
|
||||
items: []model.Component{},
|
||||
pop: 3,
|
||||
e: true,
|
||||
},
|
||||
"full": {
|
||||
items: []model.Component{c{}, c{}, top},
|
||||
pop: 3,
|
||||
e: true,
|
||||
},
|
||||
"pop": {
|
||||
items: []model.Component{c{}, c{}, top},
|
||||
pop: 2,
|
||||
e: false,
|
||||
top: top,
|
||||
},
|
||||
}
|
||||
|
||||
for k, u := range uu {
|
||||
t.Run(k, func(t *testing.T) {
|
||||
s := model.NewStack()
|
||||
for _, c := range u.items {
|
||||
s.Push(c)
|
||||
}
|
||||
for i := 0; i < u.pop; i++ {
|
||||
s.Pop()
|
||||
}
|
||||
assert.Equal(t, u.e, s.Empty())
|
||||
if !u.e {
|
||||
assert.Equal(t, u.top, s.Top())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStackTop(t *testing.T) {
|
||||
top := c{}
|
||||
uu := map[string]struct {
|
||||
items []model.Component
|
||||
e model.Component
|
||||
}{
|
||||
"blank": {
|
||||
items: []model.Component{},
|
||||
},
|
||||
"push3": {
|
||||
items: []model.Component{c{}, c{}, top},
|
||||
e: top,
|
||||
},
|
||||
"push1": {
|
||||
items: []model.Component{top},
|
||||
e: top,
|
||||
},
|
||||
}
|
||||
|
||||
for k, u := range uu {
|
||||
t.Run(k, func(t *testing.T) {
|
||||
s := model.NewStack()
|
||||
for _, item := range u.items {
|
||||
s.Push(item)
|
||||
}
|
||||
v := s.Top()
|
||||
assert.Equal(t, u.e, v)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStackListener(t *testing.T) {
|
||||
items := []model.Component{c{}, c{}, c{}}
|
||||
s := model.NewStack()
|
||||
l := stackL{}
|
||||
s.AddListener(&l)
|
||||
for _, item := range items {
|
||||
s.Push(item)
|
||||
}
|
||||
assert.Equal(t, 3, l.count)
|
||||
|
||||
for range items {
|
||||
s.Pop()
|
||||
}
|
||||
assert.Equal(t, 0, l.count)
|
||||
}
|
||||
|
||||
func TestStackRemoveListener(t *testing.T) {
|
||||
s := model.NewStack()
|
||||
l1, l2, l3 := &stackL{}, &stackL{}, &stackL{}
|
||||
s.AddListener(l1)
|
||||
s.AddListener(l2)
|
||||
s.AddListener(l3)
|
||||
|
||||
s.RemoveListener(l2)
|
||||
s.RemoveListener(l3)
|
||||
s.RemoveListener(l1)
|
||||
|
||||
s.Push(c{})
|
||||
|
||||
assert.Equal(t, 0, l1.count)
|
||||
assert.Equal(t, 0, l2.count)
|
||||
assert.Equal(t, 0, l3.count)
|
||||
}
|
||||
|
||||
type stackL struct {
|
||||
count int
|
||||
}
|
||||
|
||||
func (s *stackL) StackPushed(model.Component) {
|
||||
s.count++
|
||||
}
|
||||
func (s *stackL) StackPopped(c, top model.Component) {
|
||||
s.count--
|
||||
}
|
||||
func (s *stackL) StackTop(model.Component) {}
|
||||
|
||||
type c struct{}
|
||||
|
||||
func (c c) Name() string { return "test" }
|
||||
func (c c) Hints() model.MenuHints { return nil }
|
||||
func (c c) Draw(tcell.Screen) {}
|
||||
func (c c) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { return nil }
|
||||
func (c c) SetRect(int, int, int, int) {}
|
||||
func (c c) GetRect() (int, int, int, int) { return 0, 0, 0, 0 }
|
||||
func (c c) GetFocusable() tview.Focusable { return nil }
|
||||
func (c c) Focus(func(tview.Primitive)) {}
|
||||
func (c c) Blur() {}
|
||||
func (c c) Start() {}
|
||||
func (c c) Stop() {}
|
||||
func (c c) Init(context.Context) {}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
)
|
||||
|
||||
// TableListener tracks tabular data changes.
|
||||
type TableListener interface {
|
||||
Refreshed(resource.TableData)
|
||||
RowAdded(resource.RowEvent)
|
||||
RowUpdated(resource.RowEvent)
|
||||
RowDeleted(resource.RowEvent)
|
||||
}
|
||||
|
||||
// Table represents tabular data.
|
||||
type Table struct {
|
||||
data resource.TableData
|
||||
|
||||
listeners []TableListener
|
||||
}
|
||||
|
||||
// NewTable returns a new table.
|
||||
func NewTable() *Table {
|
||||
return &Table{}
|
||||
}
|
||||
|
||||
// Load the initial tabular data
|
||||
func (t *Table) Load(data resource.TableData) {
|
||||
t.data = data
|
||||
t.fireTableRefreshed()
|
||||
}
|
||||
|
||||
func (t *Table) fireTableRefreshed() {
|
||||
for _, l := range t.listeners {
|
||||
l.Refreshed(t.data)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/derailed/tview"
|
||||
)
|
||||
|
||||
// Igniter represents a runnable view.
|
||||
type Igniter interface {
|
||||
// Start starts a component.
|
||||
Init(ctx context.Context)
|
||||
|
||||
// Start starts a component.
|
||||
Start()
|
||||
|
||||
// Stop terminates a component.
|
||||
Stop()
|
||||
}
|
||||
|
||||
// Hinter represent a menu mnemonic provider.
|
||||
type Hinter interface {
|
||||
Hints() MenuHints
|
||||
}
|
||||
|
||||
// Primitive represents a UI primitive.
|
||||
type Primitive interface {
|
||||
tview.Primitive
|
||||
|
||||
// Name returns the view name.
|
||||
Name() string
|
||||
}
|
||||
|
||||
// Component represents a ui component
|
||||
type Component interface {
|
||||
Primitive
|
||||
Igniter
|
||||
Hinter
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package ui
|
|||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
|
@ -28,18 +29,18 @@ func NewKeyAction(d string, a ActionHandler, display bool) KeyAction {
|
|||
}
|
||||
|
||||
// Hints returns a collection of hints.
|
||||
func (a KeyActions) Hints() Hints {
|
||||
func (a KeyActions) Hints() model.MenuHints {
|
||||
kk := make([]int, 0, len(a))
|
||||
for k := range a {
|
||||
kk = append(kk, int(k))
|
||||
}
|
||||
sort.Ints(kk)
|
||||
|
||||
hh := make(Hints, 0, len(kk))
|
||||
hh := make(model.MenuHints, 0, len(kk))
|
||||
for _, k := range kk {
|
||||
if name, ok := tcell.KeyNames[tcell.Key(k)]; ok {
|
||||
hh = append(hh,
|
||||
Hint{
|
||||
model.MenuHint{
|
||||
Mnemonic: name,
|
||||
Description: a[tcell.Key(k)].Description,
|
||||
Visible: a[tcell.Key(k)].Visible,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package ui_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestKeyActionsHints(t *testing.T) {
|
||||
kk := ui.KeyActions{
|
||||
ui.KeyF: ui.NewKeyAction("fred", nil, true),
|
||||
ui.KeyB: ui.NewKeyAction("blee", nil, true),
|
||||
ui.KeyZ: ui.NewKeyAction("zorg", nil, false),
|
||||
}
|
||||
|
||||
hh := kk.Hints()
|
||||
|
||||
assert.Equal(t, 3, len(hh))
|
||||
assert.Equal(t, model.MenuHint{"b", "blee", true}, hh[0])
|
||||
}
|
||||
|
|
@ -1,87 +1,47 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
// Igniter represents an initializable view.
|
||||
type Igniter interface {
|
||||
tview.Primitive
|
||||
// App represents an application.
|
||||
type App struct {
|
||||
*tview.Application
|
||||
Configurator
|
||||
|
||||
// Init initializes the view.
|
||||
Init(ctx context.Context, ns string)
|
||||
Main *Pages
|
||||
Hint *model.Hint
|
||||
|
||||
actions KeyActions
|
||||
|
||||
views map[string]tview.Primitive
|
||||
cmdBuff *CmdBuff
|
||||
}
|
||||
|
||||
type (
|
||||
keyHandler interface {
|
||||
keyboard(evt *tcell.EventKey) *tcell.EventKey
|
||||
}
|
||||
|
||||
// ActionsFunc augments Keybindings.
|
||||
ActionsFunc func(KeyActions)
|
||||
|
||||
// Configurator represents an application configurations.
|
||||
Configurator struct {
|
||||
HasSkins bool
|
||||
Config *config.Config
|
||||
Styles *config.Styles
|
||||
Bench *config.Bench
|
||||
}
|
||||
|
||||
// App represents an application.
|
||||
App struct {
|
||||
*tview.Application
|
||||
Configurator
|
||||
|
||||
actions KeyActions
|
||||
pages *tview.Pages
|
||||
content *tview.Pages
|
||||
views map[string]tview.Primitive
|
||||
cmdBuff *CmdBuff
|
||||
hints Hints
|
||||
}
|
||||
)
|
||||
|
||||
// NewApp returns a new app.
|
||||
func NewApp() *App {
|
||||
s := App{
|
||||
a := App{
|
||||
Application: tview.NewApplication(),
|
||||
actions: make(KeyActions),
|
||||
pages: tview.NewPages(),
|
||||
content: tview.NewPages(),
|
||||
Main: NewPages(),
|
||||
cmdBuff: NewCmdBuff(':', CommandBuff),
|
||||
Hint: model.NewHint(),
|
||||
}
|
||||
|
||||
s.RefreshStyles()
|
||||
a.RefreshStyles()
|
||||
|
||||
s.views = map[string]tview.Primitive{
|
||||
"menu": NewMenuView(s.Styles),
|
||||
"logo": NewLogoView(s.Styles),
|
||||
"cmd": NewCmdView(s.Styles),
|
||||
"crumbs": NewCrumbsView(s.Styles),
|
||||
a.views = map[string]tview.Primitive{
|
||||
"menu": NewMenu(a.Styles),
|
||||
"logo": NewLogoView(a.Styles),
|
||||
"cmd": NewCmdView(a.Styles),
|
||||
"flash": NewFlashView(&a, "Initializing..."),
|
||||
"crumbs": NewCrumbs(a.Styles),
|
||||
}
|
||||
|
||||
return &s
|
||||
}
|
||||
|
||||
// Main returns main app frame.
|
||||
func (a *App) Main() *tview.Pages {
|
||||
return a.pages
|
||||
}
|
||||
|
||||
// Frame returns main app content frame.
|
||||
func (a *App) Frame() *tview.Pages {
|
||||
return a.content
|
||||
}
|
||||
|
||||
// Conn returns an api server connection.
|
||||
func (a *App) Conn() k8s.Connection {
|
||||
return a.Config.GetConnection()
|
||||
return &a
|
||||
}
|
||||
|
||||
// Init initializes the application.
|
||||
|
|
@ -89,7 +49,14 @@ func (a *App) Init() {
|
|||
a.bindKeys()
|
||||
a.SetInputCapture(a.keyboard)
|
||||
a.cmdBuff.AddListener(a.Cmd())
|
||||
a.SetRoot(a.pages, true)
|
||||
a.SetRoot(a.Main, true)
|
||||
|
||||
a.Hint.AddListener(a.Menu())
|
||||
}
|
||||
|
||||
// Conn returns an api server connection.
|
||||
func (a *App) Conn() k8s.Connection {
|
||||
return a.Config.GetConnection()
|
||||
}
|
||||
|
||||
func (a *App) bindKeys() {
|
||||
|
|
@ -148,19 +115,19 @@ func (a *App) InCmdMode() bool {
|
|||
return a.Cmd().InCmdMode()
|
||||
}
|
||||
|
||||
// GetActions returns a collection of actions.
|
||||
// GetActions returns a collection of actiona.
|
||||
func (a *App) GetActions() KeyActions {
|
||||
return a.actions
|
||||
}
|
||||
|
||||
// AddActions returns the application actions.
|
||||
// AddActions returns the application actiona.
|
||||
func (a *App) AddActions(aa KeyActions) {
|
||||
for k, v := range aa {
|
||||
a.actions[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Views return the application root views.
|
||||
// Views return the application root viewa.
|
||||
func (a *App) Views() map[string]tview.Primitive {
|
||||
return a.views
|
||||
}
|
||||
|
|
@ -215,33 +182,17 @@ func (a *App) redrawCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
return evt
|
||||
}
|
||||
|
||||
// ActiveView returns the currently active view.
|
||||
func (a *App) ActiveView() Igniter {
|
||||
return a.content.GetPrimitive("main").(Igniter)
|
||||
}
|
||||
|
||||
// SetHints updates menu hints.
|
||||
func (a *App) SetHints(h Hints) {
|
||||
a.hints = h
|
||||
a.views["menu"].(*MenuView).HydrateMenu(h)
|
||||
}
|
||||
|
||||
// GetHints retrieves the currently active hints.
|
||||
func (a *App) GetHints() Hints {
|
||||
return a.hints
|
||||
}
|
||||
|
||||
// StatusReset reset log back to normal.
|
||||
func (a *App) StatusReset() {
|
||||
a.Logo().Reset()
|
||||
a.Draw()
|
||||
}
|
||||
|
||||
// View Accessors...
|
||||
// View Accessora...
|
||||
|
||||
// Crumbs return app crumbs.
|
||||
func (a *App) Crumbs() *CrumbsView {
|
||||
return a.views["crumbs"].(*CrumbsView)
|
||||
// Crumbs return app crumba.
|
||||
func (a *App) Crumbs() *Crumbs {
|
||||
return a.views["crumbs"].(*Crumbs)
|
||||
}
|
||||
|
||||
// Logo return the app logo.
|
||||
|
|
@ -260,8 +211,8 @@ func (a *App) Cmd() *CmdView {
|
|||
}
|
||||
|
||||
// Menu returns app menu.
|
||||
func (a *App) Menu() *MenuView {
|
||||
return a.views["menu"].(*MenuView)
|
||||
func (a *App) Menu() *Menu {
|
||||
return a.views["menu"].(*Menu)
|
||||
}
|
||||
|
||||
// AsKey converts rune to keyboard key.,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
package ui_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAppGetCmd(t *testing.T) {
|
||||
a := ui.NewApp()
|
||||
a.Init()
|
||||
a.CmdBuff().Set("blee")
|
||||
|
||||
assert.Equal(t, "blee", a.GetCmd())
|
||||
}
|
||||
|
||||
func TestAppInCmdMode(t *testing.T) {
|
||||
a := ui.NewApp()
|
||||
a.Init()
|
||||
a.CmdBuff().Set("blee")
|
||||
assert.False(t, a.InCmdMode())
|
||||
|
||||
a.CmdBuff().SetActive(true)
|
||||
assert.True(t, a.InCmdMode())
|
||||
}
|
||||
|
||||
func TestAppResetCmd(t *testing.T) {
|
||||
a := ui.NewApp()
|
||||
a.Init()
|
||||
a.CmdBuff().Set("blee")
|
||||
|
||||
a.ResetCmd()
|
||||
|
||||
assert.Equal(t, "", a.CmdBuff().String())
|
||||
}
|
||||
|
||||
func TestAppHasCmd(t *testing.T) {
|
||||
a := ui.NewApp()
|
||||
a.Init()
|
||||
|
||||
a.ActivateCmd(true)
|
||||
assert.False(t, a.HasCmd())
|
||||
|
||||
a.CmdBuff().Set("blee")
|
||||
assert.True(t, a.InCmdMode())
|
||||
}
|
||||
|
||||
func TestAppGetActions(t *testing.T) {
|
||||
a := ui.NewApp()
|
||||
a.Init()
|
||||
|
||||
a.AddActions(ui.KeyActions{ui.KeyZ: ui.KeyAction{Description: "zorg"}})
|
||||
|
||||
assert.Equal(t, 8, len(a.GetActions()))
|
||||
}
|
||||
|
||||
func TestAppViews(t *testing.T) {
|
||||
a := ui.NewApp()
|
||||
a.Init()
|
||||
|
||||
for _, v := range []string{"crumbs", "logo", "cmd", "flash", "menu"} {
|
||||
t.Run(v, func(t *testing.T) {
|
||||
assert.NotNil(t, a.Views()[v])
|
||||
})
|
||||
}
|
||||
|
||||
assert.NotNil(t, a.Crumbs())
|
||||
assert.NotNil(t, a.Flash())
|
||||
assert.NotNil(t, a.Logo())
|
||||
assert.NotNil(t, a.Cmd())
|
||||
assert.NotNil(t, a.Menu())
|
||||
}
|
||||
|
|
@ -23,16 +23,14 @@ type CmdView struct {
|
|||
// NewCmdView returns a new command view.
|
||||
func NewCmdView(styles *config.Styles) *CmdView {
|
||||
v := CmdView{styles: styles, TextView: tview.NewTextView()}
|
||||
{
|
||||
v.SetWordWrap(true)
|
||||
v.SetWrap(true)
|
||||
v.SetDynamicColors(true)
|
||||
v.SetBorder(true)
|
||||
v.SetBorderPadding(0, 0, 1, 1)
|
||||
v.SetBackgroundColor(styles.BgColor())
|
||||
// v.SetBorderColor(config.AsColor(styles.Frame().Border.FocusColor))
|
||||
v.SetTextColor(styles.FgColor())
|
||||
}
|
||||
v.SetWordWrap(true)
|
||||
v.SetWrap(true)
|
||||
v.SetDynamicColors(true)
|
||||
v.SetBorder(true)
|
||||
v.SetBorderPadding(0, 0, 1, 1)
|
||||
v.SetBackgroundColor(styles.BgColor())
|
||||
v.SetTextColor(styles.FgColor())
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +50,7 @@ func (v *CmdView) update(s string) {
|
|||
}
|
||||
|
||||
func (v *CmdView) append(r rune) {
|
||||
fmt.Fprintf(v, string(r))
|
||||
fmt.Fprintf(v, "%s", string(r))
|
||||
}
|
||||
|
||||
func (v *CmdView) write(s string) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
package ui
|
||||
package ui_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
@ -16,7 +17,7 @@ func (l *testListener) BufferChanged(s string) {
|
|||
l.text = s
|
||||
}
|
||||
|
||||
func (l *testListener) BufferActive(s bool, _ BufferKind) {
|
||||
func (l *testListener) BufferActive(s bool, _ ui.BufferKind) {
|
||||
if s {
|
||||
l.act++
|
||||
return
|
||||
|
|
@ -25,27 +26,27 @@ func (l *testListener) BufferActive(s bool, _ BufferKind) {
|
|||
}
|
||||
|
||||
func TestCmdBuffActivate(t *testing.T) {
|
||||
b, l := NewCmdBuff('>', CommandBuff), testListener{}
|
||||
b, l := ui.NewCmdBuff('>', ui.CommandBuff), testListener{}
|
||||
b.AddListener(&l)
|
||||
|
||||
b.SetActive(true)
|
||||
assert.Equal(t, 1, l.act)
|
||||
assert.Equal(t, 0, l.inact)
|
||||
assert.True(t, b.active)
|
||||
assert.True(t, b.IsActive())
|
||||
}
|
||||
|
||||
func TestCmdBuffDeactivate(t *testing.T) {
|
||||
b, l := NewCmdBuff('>', CommandBuff), testListener{}
|
||||
b, l := ui.NewCmdBuff('>', ui.CommandBuff), testListener{}
|
||||
b.AddListener(&l)
|
||||
|
||||
b.SetActive(false)
|
||||
assert.Equal(t, 0, l.act)
|
||||
assert.Equal(t, 1, l.inact)
|
||||
assert.False(t, b.active)
|
||||
assert.False(t, b.IsActive())
|
||||
}
|
||||
|
||||
func TestCmdBuffChanged(t *testing.T) {
|
||||
b, l := NewCmdBuff('>', CommandBuff), testListener{}
|
||||
b, l := ui.NewCmdBuff('>', ui.CommandBuff), testListener{}
|
||||
b.AddListener(&l)
|
||||
|
||||
b.Add('b')
|
||||
|
|
@ -77,7 +78,7 @@ func TestCmdBuffChanged(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCmdBuffAdd(t *testing.T) {
|
||||
b := NewCmdBuff('>', CommandBuff)
|
||||
b := ui.NewCmdBuff('>', ui.CommandBuff)
|
||||
|
||||
uu := []struct {
|
||||
runes []rune
|
||||
|
|
@ -98,7 +99,7 @@ func TestCmdBuffAdd(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCmdBuffDel(t *testing.T) {
|
||||
b := NewCmdBuff('>', CommandBuff)
|
||||
b := ui.NewCmdBuff('>', ui.CommandBuff)
|
||||
|
||||
uu := []struct {
|
||||
runes []rune
|
||||
|
|
@ -120,7 +121,7 @@ func TestCmdBuffDel(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCmdBuffEmpty(t *testing.T) {
|
||||
b := NewCmdBuff('>', CommandBuff)
|
||||
b := ui.NewCmdBuff('>', ui.CommandBuff)
|
||||
|
||||
uu := []struct {
|
||||
runes []rune
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
package ui
|
||||
|
||||
const maxStackSize = 10
|
||||
|
||||
// CmdStack tracks users command breadcrumbs.
|
||||
type CmdStack struct {
|
||||
index int
|
||||
stack []string
|
||||
}
|
||||
|
||||
// NewCmdStack returns a new cmd stack.
|
||||
func NewCmdStack() *CmdStack {
|
||||
return &CmdStack{stack: make([]string, 0, maxStackSize)}
|
||||
}
|
||||
|
||||
// Items returns current stack content.
|
||||
func (s *CmdStack) Items() []string {
|
||||
return s.stack
|
||||
}
|
||||
|
||||
// Push adds a new item,
|
||||
func (s *CmdStack) Push(cmd string) {
|
||||
if len(s.stack) == maxStackSize {
|
||||
s.stack = s.stack[1 : len(s.stack)-1]
|
||||
}
|
||||
s.stack = append(s.stack, cmd)
|
||||
}
|
||||
|
||||
// Pop delete an item.
|
||||
func (s *CmdStack) Pop() (string, bool) {
|
||||
if s.Empty() {
|
||||
return "", false
|
||||
}
|
||||
|
||||
top := s.stack[len(s.stack)-1]
|
||||
s.stack = s.stack[:len(s.stack)-1]
|
||||
|
||||
return top, true
|
||||
}
|
||||
|
||||
// Top return top element.
|
||||
func (s *CmdStack) Top() (string, bool) {
|
||||
if s.Empty() {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return s.stack[len(s.stack)-1], true
|
||||
}
|
||||
|
||||
// Empty check if stack is empty.
|
||||
func (s *CmdStack) Empty() bool {
|
||||
return len(s.stack) == 0
|
||||
}
|
||||
|
||||
// Last returns the last command.
|
||||
func (s *CmdStack) Last() bool {
|
||||
return len(s.stack) == 1
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zerolog.SetGlobalLevel(zerolog.FatalLevel)
|
||||
}
|
||||
|
||||
func TestCmdStackPushMax(t *testing.T) {
|
||||
s := NewCmdStack()
|
||||
for i := 0; i < 20; i++ {
|
||||
s.Push(fmt.Sprintf("cmd_%d", i))
|
||||
}
|
||||
top, ok := s.Top()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "cmd_19", top)
|
||||
}
|
||||
|
||||
func TestCmdStackPop(t *testing.T) {
|
||||
type expect struct {
|
||||
val string
|
||||
ok bool
|
||||
}
|
||||
|
||||
uu := []struct {
|
||||
cmds []string
|
||||
popCount int
|
||||
e expect
|
||||
}{
|
||||
{[]string{}, 2, expect{"", false}},
|
||||
{[]string{"a", "b", "c"}, 2, expect{"a", true}},
|
||||
{[]string{"a", "b", "c"}, 1, expect{"b", true}},
|
||||
}
|
||||
|
||||
for _, u := range uu {
|
||||
s := NewCmdStack()
|
||||
for _, v := range u.cmds {
|
||||
s.Push(v)
|
||||
}
|
||||
for i := 0; i < u.popCount; i++ {
|
||||
s.Pop()
|
||||
}
|
||||
top, ok := s.Pop()
|
||||
assert.Equal(t, u.e.ok, ok)
|
||||
assert.Equal(t, u.e.val, top)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdStackEmpty(t *testing.T) {
|
||||
uu := []struct {
|
||||
cmds []string
|
||||
popCount int
|
||||
e bool
|
||||
}{
|
||||
{[]string{}, 0, true},
|
||||
{[]string{"a", "b", "c"}, 0, false},
|
||||
{[]string{"a", "b", "c"}, 3, true},
|
||||
}
|
||||
|
||||
for _, u := range uu {
|
||||
s := NewCmdStack()
|
||||
for _, v := range u.cmds {
|
||||
s.Push(v)
|
||||
}
|
||||
for i := 0; i < u.popCount; i++ {
|
||||
s.Pop()
|
||||
}
|
||||
assert.Equal(t, u.e, s.Empty())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +1,47 @@
|
|||
package ui
|
||||
package ui_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewCmdUpdate(t *testing.T) {
|
||||
func TestCmdNew(t *testing.T) {
|
||||
defaults, _ := config.NewStyles("")
|
||||
v := NewCmdView(defaults)
|
||||
v.update("blee")
|
||||
v := ui.NewCmdView(defaults)
|
||||
|
||||
buff := ui.NewCmdBuff(':', ui.CommandBuff)
|
||||
buff.AddListener(v)
|
||||
buff.Set("blee")
|
||||
|
||||
assert.Equal(t, "\x00> blee\n", v.GetText(false))
|
||||
}
|
||||
|
||||
func TestCmdInCmdMode(t *testing.T) {
|
||||
func TestCmdUpdate(t *testing.T) {
|
||||
defaults, _ := config.NewStyles("")
|
||||
v := NewCmdView(defaults)
|
||||
v.update("blee")
|
||||
v.append('!')
|
||||
v := ui.NewCmdView(defaults)
|
||||
|
||||
buff := ui.NewCmdBuff(':', ui.CommandBuff)
|
||||
buff.AddListener(v)
|
||||
|
||||
buff.Set("blee")
|
||||
buff.Add('!')
|
||||
|
||||
assert.Equal(t, "\x00> blee!\n", v.GetText(false))
|
||||
assert.False(t, v.InCmdMode())
|
||||
v.BufferActive(true, CommandBuff)
|
||||
assert.True(t, v.InCmdMode())
|
||||
}
|
||||
|
||||
func TestCmdMode(t *testing.T) {
|
||||
defaults, _ := config.NewStyles("")
|
||||
v := ui.NewCmdView(defaults)
|
||||
|
||||
buff := ui.NewCmdBuff(':', ui.CommandBuff)
|
||||
buff.AddListener(v)
|
||||
|
||||
for _, f := range []bool{false, true} {
|
||||
buff.SetActive(f)
|
||||
assert.Equal(t, f, v.InCmdMode())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
package ui_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
func TestDefaultColorer(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
re resource.RowEvent
|
||||
e tcell.Color
|
||||
}{
|
||||
"def": {resource.RowEvent{}, ui.StdColor},
|
||||
"new": {resource.RowEvent{Action: resource.New}, ui.AddColor},
|
||||
"add": {resource.RowEvent{Action: watch.Added}, ui.AddColor},
|
||||
"upd": {resource.RowEvent{Action: watch.Modified}, ui.ModColor},
|
||||
}
|
||||
|
||||
for k, u := range uu {
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, ui.DefaultColorer("", &u.re))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -10,11 +10,20 @@ import (
|
|||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Synchronizer manages ui event queue.
|
||||
type synchronizer interface {
|
||||
QueueUpdateDraw(func()) *tview.Application
|
||||
QueueUpdate(func()) *tview.Application
|
||||
}
|
||||
|
||||
// Configurator represents an application configurationa.
|
||||
type Configurator struct {
|
||||
HasSkins bool
|
||||
Config *config.Config
|
||||
Styles *config.Styles
|
||||
Bench *config.Bench
|
||||
}
|
||||
|
||||
// StylesUpdater watches for skin file changes.
|
||||
func (c *Configurator) StylesUpdater(ctx context.Context, s synchronizer) error {
|
||||
w, err := fsnotify.NewWatcher()
|
||||
|
|
@ -51,6 +60,11 @@ func (c *Configurator) InitBench(cluster string) {
|
|||
}
|
||||
}
|
||||
|
||||
// BenchConfig location of the benchmarks configuration file.
|
||||
func BenchConfig(cluster string) string {
|
||||
return filepath.Join(config.K9sHome, config.K9sBench+"-"+cluster+".yml")
|
||||
}
|
||||
|
||||
// RefreshStyles load for skin configuration changes.
|
||||
func (c *Configurator) RefreshStyles() {
|
||||
var err error
|
||||
|
|
@ -69,8 +83,3 @@ func (c *Configurator) RefreshStyles() {
|
|||
HighlightColor = config.AsColor(c.Styles.Frame().Status.HighlightColor)
|
||||
CompletedColor = config.AsColor(c.Styles.Frame().Status.CompletedColor)
|
||||
}
|
||||
|
||||
// BenchConfig location of the benchmarks configuration file.
|
||||
func BenchConfig(cluster string) string {
|
||||
return filepath.Join(config.K9sHome, config.K9sBench+"-"+cluster+".yml")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package ui_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBenchConfig(t *testing.T) {
|
||||
config.K9sHome = "/tmp/blee"
|
||||
assert.Equal(t, "/tmp/blee/bench-fred.yml", ui.BenchConfig("fred"))
|
||||
}
|
||||
|
||||
func TestConfiguratorRefreshStyle(t *testing.T) {
|
||||
config.K9sStylesFile = filepath.Join("..", "config", "test_assets", "black_and_wtf.yml")
|
||||
|
||||
cfg := ui.Configurator{}
|
||||
cfg.RefreshStyles()
|
||||
|
||||
assert.True(t, cfg.HasSkins)
|
||||
assert.Equal(t, tcell.ColorGhostWhite, ui.StdColor)
|
||||
assert.Equal(t, tcell.ColorWhiteSmoke, ui.ErrColor)
|
||||
}
|
||||
|
||||
func TestInitBench(t *testing.T) {
|
||||
config.K9sHome = filepath.Join("..", "config", "test_assets")
|
||||
|
||||
cfg := ui.Configurator{}
|
||||
cfg.InitBench("fred")
|
||||
|
||||
assert.NotNil(t, cfg.Bench)
|
||||
assert.Equal(t, 2, cfg.Bench.Benchmarks.Defaults.C)
|
||||
assert.Equal(t, 1000, cfg.Bench.Benchmarks.Defaults.N)
|
||||
assert.Equal(t, 2, len(cfg.Bench.Benchmarks.Services))
|
||||
}
|
||||
|
|
@ -4,31 +4,53 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// CrumbsView represents user breadcrumbs.
|
||||
type CrumbsView struct {
|
||||
// Crumbs represents user breadcrumbs.
|
||||
type Crumbs struct {
|
||||
*tview.TextView
|
||||
|
||||
styles *config.Styles
|
||||
stack *model.Stack
|
||||
}
|
||||
|
||||
// NewCrumbsView returns a new breadcrumb view.
|
||||
func NewCrumbsView(styles *config.Styles) *CrumbsView {
|
||||
v := CrumbsView{styles: styles, TextView: tview.NewTextView()}
|
||||
{
|
||||
v.SetBackgroundColor(styles.BgColor())
|
||||
v.SetTextAlign(tview.AlignLeft)
|
||||
v.SetBorderPadding(0, 0, 1, 1)
|
||||
v.SetDynamicColors(true)
|
||||
// NewCrumbs returns a new breadcrumb view.
|
||||
func NewCrumbs(styles *config.Styles) *Crumbs {
|
||||
v := Crumbs{
|
||||
stack: model.NewStack(),
|
||||
styles: styles,
|
||||
TextView: tview.NewTextView(),
|
||||
}
|
||||
v.SetBackgroundColor(styles.BgColor())
|
||||
v.SetTextAlign(tview.AlignLeft)
|
||||
v.SetBorderPadding(0, 0, 1, 1)
|
||||
v.SetDynamicColors(true)
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
// StackPushed indicates a new item was added.
|
||||
func (v *Crumbs) StackPushed(c model.Component) {
|
||||
v.stack.Push(c)
|
||||
log.Debug().Msgf(">>> PUSH %v", v.stack.Flatten())
|
||||
v.refresh(v.stack.Flatten())
|
||||
}
|
||||
|
||||
// StackPopped indicates an item was deleted
|
||||
func (v *Crumbs) StackPopped(_, _ model.Component) {
|
||||
v.stack.Pop()
|
||||
log.Debug().Msgf("<<< POP %v", v.stack.Flatten())
|
||||
v.refresh(v.stack.Flatten())
|
||||
}
|
||||
|
||||
// StackTop indicates the top of the stack
|
||||
func (v *Crumbs) StackTop(top model.Component) {}
|
||||
|
||||
// Refresh updates view with new crumbs.
|
||||
func (v *CrumbsView) Refresh(crumbs []string) {
|
||||
func (v *Crumbs) refresh(crumbs []string) {
|
||||
v.Clear()
|
||||
last, bgColor := len(crumbs)-1, v.styles.Frame().Crumb.BgColor
|
||||
for i, c := range crumbs {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,52 @@
|
|||
package ui
|
||||
package ui_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zerolog.SetGlobalLevel(zerolog.FatalLevel)
|
||||
}
|
||||
|
||||
func TestNewCrumbs(t *testing.T) {
|
||||
defaults, _ := config.NewStyles("")
|
||||
v := NewCrumbsView(defaults)
|
||||
v.Refresh([]string{"blee", "duh"})
|
||||
v := ui.NewCrumbs(defaults)
|
||||
v.StackPushed(makeComponent("c1"))
|
||||
v.StackPushed(makeComponent("c2"))
|
||||
v.StackPushed(makeComponent("c3"))
|
||||
|
||||
assert.Equal(t, "[black:aqua:b] <blee> [-:black:-] [black:orange:b] <duh> [-:black:-] \n", v.GetText(false))
|
||||
assert.Equal(t, "[black:aqua:b] <c1> [-:black:-] [black:aqua:b] <c2> [-:black:-] [black:orange:b] <c3> [-:black:-] \n", v.GetText(false))
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
||||
type c struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func makeComponent(n string) c {
|
||||
return c{name: n}
|
||||
}
|
||||
|
||||
func (c c) HasFocus() bool { return true }
|
||||
func (c c) Hints() model.MenuHints { return nil }
|
||||
func (c c) Name() string { return c.name }
|
||||
func (c c) Draw(tcell.Screen) {}
|
||||
func (c c) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { return nil }
|
||||
func (c c) SetRect(int, int, int, int) {}
|
||||
func (c c) GetRect() (int, int, int, int) { return 0, 0, 0, 0 }
|
||||
func (c c) GetFocusable() tview.Focusable { return c }
|
||||
func (c c) Focus(func(tview.Primitive)) {}
|
||||
func (c c) Blur() {}
|
||||
func (c c) Start() {}
|
||||
func (c c) Stop() {}
|
||||
func (c c) Init(context.Context) {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package ui
|
||||
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
// KeyApp designates an application context.
|
||||
KeyApp = ContextKey("app")
|
||||
|
||||
// KeyStyles designates the application styles.
|
||||
KeyStyles = ContextKey("styles")
|
||||
|
||||
// KeyNamespace designates a namespace context.
|
||||
KeyNamespace = ContextKey("ns")
|
||||
)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package dialog
|
||||
|
||||
import (
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
|
@ -12,7 +13,7 @@ type (
|
|||
)
|
||||
|
||||
// ShowConfirm pops a confirmation dialog.
|
||||
func ShowConfirm(pages *tview.Pages, title, msg string, ack confirmFunc, cancel cancelFunc) {
|
||||
func ShowConfirm(pages *ui.Pages, title, msg string, ack confirmFunc, cancel cancelFunc) {
|
||||
f := tview.NewForm()
|
||||
f.SetItemPadding(0)
|
||||
f.SetButtonsAlign(tview.AlignCenter).
|
||||
|
|
@ -40,6 +41,6 @@ func ShowConfirm(pages *tview.Pages, title, msg string, ack confirmFunc, cancel
|
|||
pages.ShowPage(confirmKey)
|
||||
}
|
||||
|
||||
func dismissConfirm(pages *tview.Pages) {
|
||||
func dismissConfirm(pages *ui.Pages) {
|
||||
pages.RemovePage(confirmKey)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ package dialog
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConfirmDialog(t *testing.T) {
|
||||
a := tview.NewApplication()
|
||||
p := tview.NewPages()
|
||||
p := ui.NewPages()
|
||||
a.SetRoot(p, false)
|
||||
|
||||
ackFunc := func() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package dialog
|
||||
|
||||
import (
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
|
@ -13,7 +14,7 @@ type (
|
|||
)
|
||||
|
||||
// ShowDelete pops a resource deletion dialog.
|
||||
func ShowDelete(pages *tview.Pages, msg string, ok okFunc, cancel cancelFunc) {
|
||||
func ShowDelete(pages *ui.Pages, msg string, ok okFunc, cancel cancelFunc) {
|
||||
cascade, force := true, false
|
||||
f := tview.NewForm()
|
||||
f.SetItemPadding(0)
|
||||
|
|
@ -48,6 +49,6 @@ func ShowDelete(pages *tview.Pages, msg string, ok okFunc, cancel cancelFunc) {
|
|||
pages.ShowPage(deleteKey)
|
||||
}
|
||||
|
||||
func dismissDelete(pages *tview.Pages) {
|
||||
func dismissDelete(pages *ui.Pages) {
|
||||
pages.RemovePage(deleteKey)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@ package dialog
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDeleteDialog(t *testing.T) {
|
||||
p := tview.NewPages()
|
||||
p := ui.NewPages()
|
||||
|
||||
okFunc := func(c, f bool) {
|
||||
assert.True(t, c)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package dialog
|
|||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
|
@ -10,7 +11,7 @@ import (
|
|||
const portForwardKey = "portforward"
|
||||
|
||||
// ShowPortForward pops a port forwarding configuration dialog.
|
||||
func ShowPortForward(p *tview.Pages, port string, okFn func(lport, cport string)) {
|
||||
func ShowPortForward(p *ui.Pages, port string, okFn func(lport, cport string)) {
|
||||
f := tview.NewForm()
|
||||
f.SetItemPadding(0)
|
||||
f.SetButtonsAlign(tview.AlignCenter).
|
||||
|
|
@ -43,7 +44,7 @@ func ShowPortForward(p *tview.Pages, port string, okFn func(lport, cport string)
|
|||
}
|
||||
|
||||
// DismissPortForward dismiss the port forward dialog.
|
||||
func DismissPortForward(p *tview.Pages) {
|
||||
func DismissPortForward(p *ui.Pages) {
|
||||
p.RemovePage(portForwardKey)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@ package dialog
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPortForwardDialog(t *testing.T) {
|
||||
p := tview.NewPages()
|
||||
p := ui.NewPages()
|
||||
|
||||
okFunc := func(lport, cport string) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,12 +39,12 @@ type (
|
|||
*tview.TextView
|
||||
|
||||
cancel context.CancelFunc
|
||||
app *tview.Application
|
||||
app *App
|
||||
}
|
||||
)
|
||||
|
||||
// NewFlashView returns a new flash view.
|
||||
func NewFlashView(app *tview.Application, m string) *FlashView {
|
||||
func NewFlashView(app *App, m string) *FlashView {
|
||||
f := FlashView{app: app, TextView: tview.NewTextView()}
|
||||
f.SetTextColor(tcell.ColorAqua)
|
||||
f.SetTextAlign(tview.AlignLeft)
|
||||
|
|
|
|||
|
|
@ -1,41 +1,40 @@
|
|||
package ui
|
||||
package ui_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFlashEmoji(t *testing.T) {
|
||||
uu := []struct {
|
||||
level FlashLevel
|
||||
emoji string
|
||||
}{
|
||||
{FlashWarn, emoDoh},
|
||||
{FlashErr, emoRed},
|
||||
{FlashFatal, emoDead},
|
||||
{FlashInfo, emoHappy},
|
||||
}
|
||||
func TestFlashInfo(t *testing.T) {
|
||||
f := ui.NewFlashView(ui.NewApp(), "YO!")
|
||||
|
||||
for _, u := range uu {
|
||||
assert.Equal(t, u.emoji, flashEmoji(u.level))
|
||||
}
|
||||
f.Info("Blee")
|
||||
assert.Equal(t, "😎 Blee\n", f.GetText(false))
|
||||
|
||||
f.Infof("Blee %s", "duh")
|
||||
assert.Equal(t, "😎 Blee duh\n", f.GetText(false))
|
||||
}
|
||||
|
||||
func TestFlashColor(t *testing.T) {
|
||||
uu := []struct {
|
||||
level FlashLevel
|
||||
color tcell.Color
|
||||
}{
|
||||
{FlashWarn, tcell.ColorOrange},
|
||||
{FlashErr, tcell.ColorOrangeRed},
|
||||
{FlashFatal, tcell.ColorFuchsia},
|
||||
{FlashInfo, tcell.ColorNavajoWhite},
|
||||
}
|
||||
func TestFlashWarn(t *testing.T) {
|
||||
f := ui.NewFlashView(ui.NewApp(), "YO!")
|
||||
|
||||
for _, u := range uu {
|
||||
assert.Equal(t, u.color, flashColor(u.level))
|
||||
}
|
||||
f.Warn("Blee")
|
||||
assert.Equal(t, "😗 Blee\n", f.GetText(false))
|
||||
|
||||
f.Warnf("Blee %s", "duh")
|
||||
assert.Equal(t, "😗 Blee duh\n", f.GetText(false))
|
||||
}
|
||||
|
||||
func TestFlashErr(t *testing.T) {
|
||||
f := ui.NewFlashView(ui.NewApp(), "YO!")
|
||||
|
||||
f.Err(errors.New("Blee"))
|
||||
assert.Equal(t, "😡 Blee\n", f.GetText(false))
|
||||
|
||||
f.Errf("Blee %s", "duh")
|
||||
assert.Equal(t, "😡 Blee duh\n", f.GetText(false))
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
package ui_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIndicatorReset(t *testing.T) {
|
||||
s, _ := config.NewStyles("")
|
||||
|
||||
i := ui.NewIndicatorView(ui.NewApp(), s)
|
||||
i.SetPermanent("Blee")
|
||||
i.Info("duh")
|
||||
i.Reset()
|
||||
|
||||
assert.Equal(t, "Blee\n", i.GetText(false))
|
||||
}
|
||||
|
||||
func TestIndicatorInfo(t *testing.T) {
|
||||
s, _ := config.NewStyles("")
|
||||
|
||||
i := ui.NewIndicatorView(ui.NewApp(), s)
|
||||
i.Info("Blee")
|
||||
|
||||
assert.Equal(t, "[lawngreen::b] <Blee> \n", i.GetText(false))
|
||||
}
|
||||
|
||||
func TestIndicatorWarn(t *testing.T) {
|
||||
s, _ := config.NewStyles("")
|
||||
|
||||
i := ui.NewIndicatorView(ui.NewApp(), s)
|
||||
i.Warn("Blee")
|
||||
|
||||
assert.Equal(t, "[mediumvioletred::b] <Blee> \n", i.GetText(false))
|
||||
}
|
||||
|
||||
func TestIndicatorErr(t *testing.T) {
|
||||
s, _ := config.NewStyles("")
|
||||
|
||||
i := ui.NewIndicatorView(ui.NewApp(), s)
|
||||
i.Err("Blee")
|
||||
|
||||
assert.Equal(t, "[orangered::b] <Blee> \n", i.GetText(false))
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
package ui
|
||||
|
||||
import "github.com/gdamore/tcell"
|
||||
|
||||
func init() {
|
||||
initKeys()
|
||||
}
|
||||
|
||||
func initKeys() {
|
||||
tcell.KeyNames[tcell.Key(KeyHelp)] = "?"
|
||||
tcell.KeyNames[tcell.Key(KeySlash)] = "/"
|
||||
tcell.KeyNames[tcell.Key(KeySpace)] = "space"
|
||||
|
||||
initNumbKeys()
|
||||
initStdKeys()
|
||||
initShiftKeys()
|
||||
}
|
||||
|
||||
// Defines numeric keys for container actions
|
||||
const (
|
||||
Key0 int32 = iota + 48
|
||||
Key1
|
||||
Key2
|
||||
Key3
|
||||
Key4
|
||||
Key5
|
||||
Key6
|
||||
Key7
|
||||
Key8
|
||||
Key9
|
||||
)
|
||||
|
||||
// Defines char keystrokes
|
||||
const (
|
||||
KeyA tcell.Key = iota + 97
|
||||
KeyB
|
||||
KeyC
|
||||
KeyD
|
||||
KeyE
|
||||
KeyF
|
||||
KeyG
|
||||
KeyH
|
||||
KeyI
|
||||
KeyJ
|
||||
KeyK
|
||||
KeyL
|
||||
KeyM
|
||||
KeyN
|
||||
KeyO
|
||||
KeyP
|
||||
KeyQ
|
||||
KeyR
|
||||
KeyS
|
||||
KeyT
|
||||
KeyU
|
||||
KeyV
|
||||
KeyW
|
||||
KeyX
|
||||
KeyY
|
||||
KeyZ
|
||||
KeyHelp = 63
|
||||
KeySlash = 47
|
||||
KeyColon = 58
|
||||
KeySpace = 32
|
||||
)
|
||||
|
||||
// Define Shift Keys
|
||||
const (
|
||||
KeyShiftA tcell.Key = iota + 65
|
||||
KeyShiftB
|
||||
KeyShiftC
|
||||
KeyShiftD
|
||||
KeyShiftE
|
||||
KeyShiftF
|
||||
KeyShiftG
|
||||
KeyShiftH
|
||||
KeyShiftI
|
||||
KeyShiftJ
|
||||
KeyShiftK
|
||||
KeyShiftL
|
||||
KeyShiftM
|
||||
KeyShiftN
|
||||
KeyShiftO
|
||||
KeyShiftP
|
||||
KeyShiftQ
|
||||
KeyShiftR
|
||||
KeyShiftS
|
||||
KeyShiftT
|
||||
KeyShiftU
|
||||
KeyShiftV
|
||||
KeyShiftW
|
||||
KeyShiftX
|
||||
KeyShiftY
|
||||
KeyShiftZ
|
||||
)
|
||||
|
||||
// NumKeys tracks number keys.
|
||||
var NumKeys = map[int]int32{
|
||||
0: Key0,
|
||||
1: Key1,
|
||||
2: Key2,
|
||||
3: Key3,
|
||||
4: Key4,
|
||||
5: Key5,
|
||||
6: Key6,
|
||||
7: Key7,
|
||||
8: Key8,
|
||||
9: Key9,
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
func initStdKeys() {
|
||||
tcell.KeyNames[tcell.Key(KeyA)] = "a"
|
||||
tcell.KeyNames[tcell.Key(KeyB)] = "b"
|
||||
tcell.KeyNames[tcell.Key(KeyC)] = "c"
|
||||
tcell.KeyNames[tcell.Key(KeyD)] = "d"
|
||||
tcell.KeyNames[tcell.Key(KeyE)] = "e"
|
||||
tcell.KeyNames[tcell.Key(KeyF)] = "f"
|
||||
tcell.KeyNames[tcell.Key(KeyG)] = "g"
|
||||
tcell.KeyNames[tcell.Key(KeyH)] = "h"
|
||||
tcell.KeyNames[tcell.Key(KeyI)] = "i"
|
||||
tcell.KeyNames[tcell.Key(KeyJ)] = "j"
|
||||
tcell.KeyNames[tcell.Key(KeyK)] = "k"
|
||||
tcell.KeyNames[tcell.Key(KeyL)] = "l"
|
||||
tcell.KeyNames[tcell.Key(KeyM)] = "m"
|
||||
tcell.KeyNames[tcell.Key(KeyN)] = "n"
|
||||
tcell.KeyNames[tcell.Key(KeyO)] = "o"
|
||||
tcell.KeyNames[tcell.Key(KeyP)] = "p"
|
||||
tcell.KeyNames[tcell.Key(KeyQ)] = "q"
|
||||
tcell.KeyNames[tcell.Key(KeyR)] = "r"
|
||||
tcell.KeyNames[tcell.Key(KeyS)] = "s"
|
||||
tcell.KeyNames[tcell.Key(KeyT)] = "t"
|
||||
tcell.KeyNames[tcell.Key(KeyU)] = "u"
|
||||
tcell.KeyNames[tcell.Key(KeyV)] = "v"
|
||||
tcell.KeyNames[tcell.Key(KeyW)] = "w"
|
||||
tcell.KeyNames[tcell.Key(KeyX)] = "x"
|
||||
tcell.KeyNames[tcell.Key(KeyY)] = "y"
|
||||
tcell.KeyNames[tcell.Key(KeyZ)] = "z"
|
||||
}
|
||||
|
||||
func initShiftKeys() {
|
||||
tcell.KeyNames[tcell.Key(KeyShiftA)] = "Shift-A"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftB)] = "Shift-B"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftC)] = "Shift-C"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftD)] = "Shift-D"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftE)] = "Shift-E"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftF)] = "Shift-F"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftG)] = "Shift-G"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftH)] = "Shift-H"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftI)] = "Shift-I"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftJ)] = "Shift-J"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftK)] = "Shift-K"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftL)] = "Shift-L"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftM)] = "Shift-M"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftN)] = "Shift-N"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftO)] = "Shift-O"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftP)] = "Shift-P"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftQ)] = "Shift-Q"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftR)] = "Shift-R"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftS)] = "Shift-S"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftT)] = "Shift-T"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftU)] = "Shift-U"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftV)] = "Shift-V"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftW)] = "Shift-W"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftX)] = "Shift-X"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftY)] = "Shift-Y"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftZ)] = "Shift-Z"
|
||||
}
|
||||
|
|
@ -9,15 +9,11 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
runewidth "github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
func init() {
|
||||
initKeys()
|
||||
}
|
||||
|
||||
const (
|
||||
menuIndexFmt = " [key:bg:b]<%d> [fg:bg:d]%s "
|
||||
maxRows = 7
|
||||
|
|
@ -25,29 +21,34 @@ const (
|
|||
|
||||
var menuRX = regexp.MustCompile(`\d`)
|
||||
|
||||
// MenuView represents menu options.
|
||||
type MenuView struct {
|
||||
// Menu presents menu options.
|
||||
type Menu struct {
|
||||
*tview.Table
|
||||
|
||||
styles *config.Styles
|
||||
}
|
||||
|
||||
// NewMenuView returns a new menu.
|
||||
func NewMenuView(styles *config.Styles) *MenuView {
|
||||
v := MenuView{Table: tview.NewTable(), styles: styles}
|
||||
// NewMenu returns a new menu.
|
||||
func NewMenu(styles *config.Styles) *Menu {
|
||||
v := Menu{Table: tview.NewTable(), styles: styles}
|
||||
v.SetBackgroundColor(styles.BgColor())
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
// HintsChanged updates the menu based on hints changing.
|
||||
func (v *Menu) HintsChanged(hh model.MenuHints) {
|
||||
v.HydrateMenu(hh)
|
||||
}
|
||||
|
||||
// HydrateMenu populate menu ui from hints.
|
||||
func (v *MenuView) HydrateMenu(hh Hints) {
|
||||
func (v *Menu) HydrateMenu(hh model.MenuHints) {
|
||||
v.Clear()
|
||||
sort.Sort(hh)
|
||||
t := v.buildMenuTable(hh)
|
||||
for row := 0; row < len(t); row++ {
|
||||
for col := 0; col < len(t[row]); col++ {
|
||||
if t[row][col] == "" {
|
||||
if len(t[row][col]) == 0 {
|
||||
continue
|
||||
}
|
||||
c := tview.NewTableCell(t[row][col])
|
||||
|
|
@ -57,42 +58,33 @@ func (v *MenuView) HydrateMenu(hh Hints) {
|
|||
}
|
||||
}
|
||||
|
||||
func isDigit(s string) bool {
|
||||
return menuRX.MatchString(s)
|
||||
}
|
||||
func (v *Menu) buildMenuTable(hh model.MenuHints) [][]string {
|
||||
table := make([]model.MenuHints, maxRows+1)
|
||||
|
||||
func (v *MenuView) buildMenuTable(hh Hints) [][]string {
|
||||
table := make([][]Hint, maxRows)
|
||||
colCount := len(hh) / maxRows
|
||||
if colCount == 0 {
|
||||
colCount = 1
|
||||
}
|
||||
if isDigit(hh[0].Mnemonic) {
|
||||
colCount++
|
||||
}
|
||||
colCount := (len(hh) / maxRows) + 1
|
||||
for row := 0; row < maxRows; row++ {
|
||||
table[row] = make([]Hint, colCount)
|
||||
table[row] = make(model.MenuHints, colCount+1)
|
||||
}
|
||||
var row, col, added int
|
||||
|
||||
var row, col int
|
||||
firstCmd := true
|
||||
maxKeys := make([]int, colCount+1)
|
||||
for _, h := range hh {
|
||||
if !h.Visible {
|
||||
continue
|
||||
}
|
||||
if !isDigit(h.Mnemonic) && firstCmd {
|
||||
isDigit := menuRX.MatchString(h.Mnemonic)
|
||||
if !isDigit && firstCmd {
|
||||
row, col, firstCmd = 0, col+1, false
|
||||
if added == 0 {
|
||||
col = 0
|
||||
}
|
||||
}
|
||||
if maxKeys[col] < len(h.Mnemonic) {
|
||||
maxKeys[col] = len(h.Mnemonic)
|
||||
}
|
||||
table[row][col] = h
|
||||
added, row = added+1, row+1
|
||||
row++
|
||||
if row >= maxRows {
|
||||
row, col = 0, col+1
|
||||
col++
|
||||
row = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -124,15 +116,20 @@ func keyConv(s string) string {
|
|||
return strings.Replace(s, "alt", "opt", 1)
|
||||
}
|
||||
|
||||
// Truncate a string to the given l and suffix ellipsis if needed.
|
||||
func Truncate(str string, width int) string {
|
||||
return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis))
|
||||
}
|
||||
|
||||
func toMnemonic(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
|
||||
return "<" + strings.ToLower(s) + ">"
|
||||
return "<" + keyConv(strings.ToLower(s)) + ">"
|
||||
}
|
||||
|
||||
func (v *MenuView) formatMenu(h Hint, size int) string {
|
||||
func (v *Menu) formatMenu(h model.MenuHint, size int) string {
|
||||
i, err := strconv.Atoi(h.Mnemonic)
|
||||
if err == nil {
|
||||
return formatNSMenu(i, h.Description, v.styles.Frame())
|
||||
|
|
@ -145,195 +142,13 @@ func formatNSMenu(i int, name string, styles config.Frame) string {
|
|||
fmat := strings.Replace(menuIndexFmt, "[key", "["+styles.Menu.NumKeyColor, 1)
|
||||
fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor+":", -1)
|
||||
fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor, 1)
|
||||
return fmt.Sprintf(fmat, i, resource.Truncate(name, 14))
|
||||
return fmt.Sprintf(fmat, i, Truncate(name, 14))
|
||||
}
|
||||
|
||||
func formatPlainMenu(h Hint, size int, styles config.Frame) string {
|
||||
func formatPlainMenu(h model.MenuHint, size int, styles config.Frame) string {
|
||||
menuFmt := " [key:bg:b]%-" + strconv.Itoa(size+2) + "s [fg:bg:d]%s "
|
||||
fmat := strings.Replace(menuFmt, "[key", "["+styles.Menu.KeyColor, 1)
|
||||
fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor, 1)
|
||||
fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor+":", -1)
|
||||
return fmt.Sprintf(fmat, toMnemonic(h.Mnemonic), h.Description)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Key mapping Constants
|
||||
|
||||
// Defines numeric keys for container actions
|
||||
const (
|
||||
Key0 int32 = iota + 48
|
||||
Key1
|
||||
Key2
|
||||
Key3
|
||||
Key4
|
||||
Key5
|
||||
Key6
|
||||
Key7
|
||||
Key8
|
||||
Key9
|
||||
)
|
||||
|
||||
// Defines char keystrokes
|
||||
const (
|
||||
KeyA tcell.Key = iota + 97
|
||||
KeyB
|
||||
KeyC
|
||||
KeyD
|
||||
KeyE
|
||||
KeyF
|
||||
KeyG
|
||||
KeyH
|
||||
KeyI
|
||||
KeyJ
|
||||
KeyK
|
||||
KeyL
|
||||
KeyM
|
||||
KeyN
|
||||
KeyO
|
||||
KeyP
|
||||
KeyQ
|
||||
KeyR
|
||||
KeyS
|
||||
KeyT
|
||||
KeyU
|
||||
KeyV
|
||||
KeyW
|
||||
KeyX
|
||||
KeyY
|
||||
KeyZ
|
||||
KeyHelp = 63
|
||||
KeySlash = 47
|
||||
KeyColon = 58
|
||||
KeySpace = 32
|
||||
)
|
||||
|
||||
// Define Shift Keys
|
||||
const (
|
||||
KeyShiftA tcell.Key = iota + 65
|
||||
KeyShiftB
|
||||
KeyShiftC
|
||||
KeyShiftD
|
||||
KeyShiftE
|
||||
KeyShiftF
|
||||
KeyShiftG
|
||||
KeyShiftH
|
||||
KeyShiftI
|
||||
KeyShiftJ
|
||||
KeyShiftK
|
||||
KeyShiftL
|
||||
KeyShiftM
|
||||
KeyShiftN
|
||||
KeyShiftO
|
||||
KeyShiftP
|
||||
KeyShiftQ
|
||||
KeyShiftR
|
||||
KeyShiftS
|
||||
KeyShiftT
|
||||
KeyShiftU
|
||||
KeyShiftV
|
||||
KeyShiftW
|
||||
KeyShiftX
|
||||
KeyShiftY
|
||||
KeyShiftZ
|
||||
)
|
||||
|
||||
// NumKeys tracks number keys.
|
||||
var NumKeys = map[int]int32{
|
||||
0: Key0,
|
||||
1: Key1,
|
||||
2: Key2,
|
||||
3: Key3,
|
||||
4: Key4,
|
||||
5: Key5,
|
||||
6: Key6,
|
||||
7: Key7,
|
||||
8: Key8,
|
||||
9: Key9,
|
||||
}
|
||||
|
||||
func initKeys() {
|
||||
tcell.KeyNames[tcell.Key(KeyHelp)] = "?"
|
||||
tcell.KeyNames[tcell.Key(KeySlash)] = "/"
|
||||
tcell.KeyNames[tcell.Key(KeySpace)] = "space"
|
||||
|
||||
initNumbKeys()
|
||||
initStdKeys()
|
||||
initShiftKeys()
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
func initStdKeys() {
|
||||
tcell.KeyNames[tcell.Key(KeyA)] = "a"
|
||||
tcell.KeyNames[tcell.Key(KeyB)] = "b"
|
||||
tcell.KeyNames[tcell.Key(KeyC)] = "c"
|
||||
tcell.KeyNames[tcell.Key(KeyD)] = "d"
|
||||
tcell.KeyNames[tcell.Key(KeyE)] = "e"
|
||||
tcell.KeyNames[tcell.Key(KeyF)] = "f"
|
||||
tcell.KeyNames[tcell.Key(KeyG)] = "g"
|
||||
tcell.KeyNames[tcell.Key(KeyH)] = "h"
|
||||
tcell.KeyNames[tcell.Key(KeyI)] = "i"
|
||||
tcell.KeyNames[tcell.Key(KeyJ)] = "j"
|
||||
tcell.KeyNames[tcell.Key(KeyK)] = "k"
|
||||
tcell.KeyNames[tcell.Key(KeyL)] = "l"
|
||||
tcell.KeyNames[tcell.Key(KeyM)] = "m"
|
||||
tcell.KeyNames[tcell.Key(KeyN)] = "n"
|
||||
tcell.KeyNames[tcell.Key(KeyO)] = "o"
|
||||
tcell.KeyNames[tcell.Key(KeyP)] = "p"
|
||||
tcell.KeyNames[tcell.Key(KeyQ)] = "q"
|
||||
tcell.KeyNames[tcell.Key(KeyR)] = "r"
|
||||
tcell.KeyNames[tcell.Key(KeyS)] = "s"
|
||||
tcell.KeyNames[tcell.Key(KeyT)] = "t"
|
||||
tcell.KeyNames[tcell.Key(KeyU)] = "u"
|
||||
tcell.KeyNames[tcell.Key(KeyV)] = "v"
|
||||
tcell.KeyNames[tcell.Key(KeyW)] = "w"
|
||||
tcell.KeyNames[tcell.Key(KeyX)] = "x"
|
||||
tcell.KeyNames[tcell.Key(KeyY)] = "y"
|
||||
tcell.KeyNames[tcell.Key(KeyZ)] = "z"
|
||||
}
|
||||
|
||||
// BOZO!! No sure why these aren't mapped??
|
||||
func initCtrlKeys() {
|
||||
tcell.KeyNames[tcell.KeyCtrlI] = "Ctrl-I"
|
||||
tcell.KeyNames[tcell.KeyCtrlM] = "Ctrl-M"
|
||||
}
|
||||
|
||||
func initShiftKeys() {
|
||||
tcell.KeyNames[tcell.Key(KeyShiftA)] = "Shift-A"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftB)] = "Shift-B"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftC)] = "Shift-C"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftD)] = "Shift-D"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftE)] = "Shift-E"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftF)] = "Shift-F"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftG)] = "Shift-G"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftH)] = "Shift-H"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftI)] = "Shift-I"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftJ)] = "Shift-J"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftK)] = "Shift-K"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftL)] = "Shift-L"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftM)] = "Shift-M"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftN)] = "Shift-N"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftO)] = "Shift-O"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftP)] = "Shift-P"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftQ)] = "Shift-Q"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftR)] = "Shift-R"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftS)] = "Shift-S"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftT)] = "Shift-T"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftU)] = "Shift-U"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftV)] = "Shift-V"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftW)] = "Shift-W"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftX)] = "Shift-X"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftY)] = "Shift-Y"
|
||||
tcell.KeyNames[tcell.Key(KeyShiftZ)] = "Shift-Z"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
package ui
|
||||
package ui_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestNewMenuView(t *testing.T) {
|
||||
func TestNewMenu(t *testing.T) {
|
||||
defaults, _ := config.NewStyles("")
|
||||
v := NewMenuView(defaults)
|
||||
v.HydrateMenu(Hints{
|
||||
{"0", "zero", true},
|
||||
{"a", "bleeA", true},
|
||||
{"b", "bleeB", true},
|
||||
v := ui.NewMenu(defaults)
|
||||
v.HydrateMenu(model.MenuHints{
|
||||
{Mnemonic: "a", Description: "bleeA", Visible: true},
|
||||
{Mnemonic: "b", Description: "bleeB", Visible: true},
|
||||
{Mnemonic: "0", Description: "zero", Visible: true},
|
||||
})
|
||||
|
||||
assert.Equal(t, " [fuchsia:black:b]<0> [white:black:d]zero ", v.GetCell(0, 0).Text)
|
||||
|
|
@ -22,23 +24,23 @@ func TestNewMenuView(t *testing.T) {
|
|||
assert.Equal(t, " [dodgerblue:black:b]<b> [white:black:d]bleeB ", v.GetCell(1, 1).Text)
|
||||
}
|
||||
|
||||
func TestKeyActions(t *testing.T) {
|
||||
func TestActionHints(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
aa KeyActions
|
||||
e Hints
|
||||
aa ui.KeyActions
|
||||
e model.MenuHints
|
||||
}{
|
||||
"a": {
|
||||
aa: KeyActions{
|
||||
KeyB: NewKeyAction("bleeB", nil, true),
|
||||
KeyA: NewKeyAction("bleeA", nil, true),
|
||||
tcell.Key(Key0): NewKeyAction("zero", nil, true),
|
||||
tcell.Key(Key1): NewKeyAction("one", nil, false),
|
||||
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),
|
||||
},
|
||||
e: Hints{
|
||||
{"0", "zero", true},
|
||||
{"1", "one", false},
|
||||
{"a", "bleeA", true},
|
||||
{"b", "bleeB", true},
|
||||
e: model.MenuHints{
|
||||
{Mnemonic: "0", Description: "zero", Visible: true},
|
||||
{Mnemonic: "1", Description: "one", Visible: false},
|
||||
{Mnemonic: "a", Description: "bleeA", Visible: true},
|
||||
{Mnemonic: "b", Description: "bleeB", Visible: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Pages struct {
|
||||
*tview.Pages
|
||||
*model.Stack
|
||||
}
|
||||
|
||||
func NewPages() *Pages {
|
||||
p := Pages{
|
||||
Pages: tview.NewPages(),
|
||||
Stack: model.NewStack(),
|
||||
}
|
||||
p.Stack.AddListener(&p)
|
||||
|
||||
return &p
|
||||
}
|
||||
|
||||
// Get fetch a page given its name.
|
||||
func (p *Pages) get(n string) model.Component {
|
||||
if comp, ok := p.GetPrimitive(n).(model.Component); ok {
|
||||
return comp
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddAndShow adds a new page and bring it to front.
|
||||
func (p *Pages) addAndShow(c model.Component) {
|
||||
p.add(c)
|
||||
p.Show(c.Name())
|
||||
}
|
||||
|
||||
// Add adds a new page.
|
||||
func (p *Pages) add(c model.Component) {
|
||||
p.AddPage(c.Name(), c, true, true)
|
||||
}
|
||||
|
||||
// Delete removes a page.
|
||||
func (p *Pages) delete(c model.Component) {
|
||||
p.RemovePage(c.Name())
|
||||
}
|
||||
|
||||
// Show brings a named page forward.
|
||||
func (p *Pages) Show(n string) {
|
||||
p.SwitchToPage(n)
|
||||
}
|
||||
|
||||
func (p *Pages) DumpPages() {
|
||||
log.Debug().Msgf("Dumping Pages %p", p)
|
||||
for i, n := range p.Stack.Flatten() {
|
||||
log.Debug().Msgf("%d -- %s -- %#v", i, n, p.GetPrimitive(n))
|
||||
}
|
||||
}
|
||||
|
||||
// Stack Protocol...
|
||||
|
||||
func (p *Pages) StackPushed(c model.Component) {
|
||||
p.addAndShow(c)
|
||||
}
|
||||
|
||||
func (p *Pages) StackPopped(o, top model.Component) {
|
||||
p.delete(o)
|
||||
}
|
||||
|
||||
func (p *Pages) StackTop(top model.Component) {
|
||||
if top == nil {
|
||||
return
|
||||
}
|
||||
p.Show(top.Name())
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package ui_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPagesPush(t *testing.T) {
|
||||
c1, c2 := makeComponent("c1"), makeComponent("c2")
|
||||
|
||||
p := ui.NewPages()
|
||||
p.Push(c1)
|
||||
p.Push(c2)
|
||||
|
||||
assert.Equal(t, 2, p.GetPageCount())
|
||||
assert.Equal(t, c2, p.CurrentPage().Item)
|
||||
}
|
||||
|
||||
func TestPagesPop(t *testing.T) {
|
||||
c1, c2 := makeComponent("c1"), makeComponent("c2")
|
||||
|
||||
p := ui.NewPages()
|
||||
p.Push(c1)
|
||||
p.Push(c2)
|
||||
p.Pop()
|
||||
|
||||
assert.Equal(t, 1, p.GetPageCount())
|
||||
assert.Equal(t, c1, p.CurrentPage().Item)
|
||||
}
|
||||
|
|
@ -9,11 +9,6 @@ import (
|
|||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
const (
|
||||
company = "imhotep.io"
|
||||
product = "Kubernetes CLI Island Style!"
|
||||
)
|
||||
|
||||
// LogoSmall K9s small log.
|
||||
var LogoSmall = []string{
|
||||
` ____ __.________ `,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
|
|
@ -9,6 +10,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
|
|
@ -46,160 +48,167 @@ type Table struct {
|
|||
}
|
||||
|
||||
// NewTable returns a new table view.
|
||||
func NewTable(title string, styles *config.Styles) *Table {
|
||||
v := Table{
|
||||
func NewTable(title string) *Table {
|
||||
return &Table{
|
||||
Table: tview.NewTable(),
|
||||
styles: styles,
|
||||
actions: make(KeyActions),
|
||||
cmdBuff: NewCmdBuff('/', FilterBuff),
|
||||
baseTitle: title,
|
||||
sortCol: SortColumn{0, 0, true},
|
||||
marks: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
v.SetFixed(1, 0)
|
||||
v.SetBorder(true)
|
||||
v.SetBackgroundColor(config.AsColor(styles.Table().BgColor))
|
||||
v.SetBorderColor(config.AsColor(styles.Table().FgColor))
|
||||
v.SetBorderFocusColor(config.AsColor(styles.Frame().Border.FocusColor))
|
||||
v.SetBorderAttributes(tcell.AttrBold)
|
||||
v.SetBorderPadding(0, 0, 1, 1)
|
||||
v.SetSelectable(true, false)
|
||||
v.SetSelectedStyle(
|
||||
func (t *Table) Init(ctx context.Context) {
|
||||
t.styles = ctx.Value(KeyStyles).(*config.Styles)
|
||||
|
||||
t.SetFixed(1, 0)
|
||||
t.SetBorder(true)
|
||||
t.SetBackgroundColor(config.AsColor(t.styles.Table().BgColor))
|
||||
t.SetBorderColor(config.AsColor(t.styles.Table().FgColor))
|
||||
t.SetBorderFocusColor(config.AsColor(t.styles.Frame().Border.FocusColor))
|
||||
t.SetBorderAttributes(tcell.AttrBold)
|
||||
t.SetBorderPadding(0, 0, 1, 1)
|
||||
t.SetSelectable(true, false)
|
||||
t.SetSelectedStyle(
|
||||
tcell.ColorBlack,
|
||||
config.AsColor(styles.Table().CursorColor),
|
||||
config.AsColor(t.styles.Table().CursorColor),
|
||||
tcell.AttrBold,
|
||||
)
|
||||
|
||||
v.SetSelectionChangedFunc(v.selChanged)
|
||||
v.SetInputCapture(v.keyboard)
|
||||
t.SetSelectionChangedFunc(t.selChanged)
|
||||
t.SetInputCapture(t.keyboard)
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
// SendKey sends an keyboard event (testing only!).
|
||||
func (t *Table) SendKey(evt *tcell.EventKey) {
|
||||
t.keyboard(evt)
|
||||
}
|
||||
|
||||
// GetRow retrieves the entire selected row.
|
||||
func (v *Table) GetRow() resource.Row {
|
||||
r := make(resource.Row, v.GetColumnCount())
|
||||
for i := 0; i < v.GetColumnCount(); i++ {
|
||||
c := v.GetCell(v.selectedRow, i)
|
||||
func (t *Table) GetRow() resource.Row {
|
||||
r := make(resource.Row, t.GetColumnCount())
|
||||
for i := 0; i < t.GetColumnCount(); i++ {
|
||||
c := t.GetCell(t.selectedRow, i)
|
||||
r[i] = strings.TrimSpace(c.Text)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// AddSelectedRowListener add a new selected row listener.
|
||||
func (v *Table) AddSelectedRowListener(f SelectedRowFunc) {
|
||||
v.selListeners = append(v.selListeners, f)
|
||||
func (t *Table) AddSelectedRowListener(f SelectedRowFunc) {
|
||||
t.selListeners = append(t.selListeners, f)
|
||||
}
|
||||
|
||||
func (v *Table) selChanged(r, c int) {
|
||||
v.selectedRow = r
|
||||
v.updateSelectedItem(r)
|
||||
func (t *Table) selChanged(r, c int) {
|
||||
t.selectedRow = r
|
||||
t.updateSelectedItem(r)
|
||||
if r == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
cell := v.GetCell(r, c)
|
||||
v.SetSelectedStyle(
|
||||
cell := t.GetCell(r, c)
|
||||
t.SetSelectedStyle(
|
||||
tcell.ColorBlack,
|
||||
cell.Color,
|
||||
tcell.AttrBold,
|
||||
)
|
||||
|
||||
for _, f := range v.selListeners {
|
||||
for _, f := range t.selListeners {
|
||||
f(r, c)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateSelection refresh selected row.
|
||||
func (v *Table) updateSelection(broadcast bool) {
|
||||
v.SelectRow(v.selectedRow, broadcast)
|
||||
func (t *Table) updateSelection(broadcast bool) {
|
||||
t.SelectRow(t.selectedRow, broadcast)
|
||||
}
|
||||
|
||||
// SelectRow select a given row by index.
|
||||
func (v *Table) SelectRow(r int, broadcast bool) {
|
||||
func (t *Table) SelectRow(r int, broadcast bool) {
|
||||
if !broadcast {
|
||||
v.SetSelectionChangedFunc(nil)
|
||||
t.SetSelectionChangedFunc(nil)
|
||||
}
|
||||
defer v.SetSelectionChangedFunc(v.selChanged)
|
||||
v.Select(r, 0)
|
||||
v.updateSelectedItem(r)
|
||||
defer t.SetSelectionChangedFunc(t.selChanged)
|
||||
t.Select(r, 0)
|
||||
t.updateSelectedItem(r)
|
||||
}
|
||||
|
||||
func (v *Table) updateSelectedItem(r int) {
|
||||
if r == 0 || v.GetCell(r, 0) == nil {
|
||||
v.selectedItem = ""
|
||||
func (t *Table) updateSelectedItem(r int) {
|
||||
if r == 0 || t.GetCell(r, 0) == nil {
|
||||
t.selectedItem = ""
|
||||
return
|
||||
}
|
||||
|
||||
col0 := TrimCell(v, r, 0)
|
||||
switch v.activeNS {
|
||||
col0 := TrimCell(t, r, 0)
|
||||
switch t.activeNS {
|
||||
case resource.NotNamespaced:
|
||||
v.selectedItem = col0
|
||||
t.selectedItem = col0
|
||||
case resource.AllNamespace, resource.AllNamespaces:
|
||||
v.selectedItem = path.Join(col0, TrimCell(v, r, 1))
|
||||
t.selectedItem = path.Join(col0, TrimCell(t, r, 1))
|
||||
default:
|
||||
v.selectedItem = path.Join(v.activeNS, col0)
|
||||
t.selectedItem = path.Join(t.activeNS, col0)
|
||||
}
|
||||
}
|
||||
|
||||
// SetSelectedFn defines a function that cleanse the current selection.
|
||||
func (v *Table) SetSelectedFn(f func(string) string) {
|
||||
v.selectedFn = f
|
||||
func (t *Table) SetSelectedFn(f func(string) string) {
|
||||
t.selectedFn = f
|
||||
}
|
||||
|
||||
// RowSelected checks if there is an active row selection.
|
||||
func (v *Table) RowSelected() bool {
|
||||
return v.selectedItem != ""
|
||||
func (t *Table) RowSelected() bool {
|
||||
return t.selectedItem != ""
|
||||
}
|
||||
|
||||
// GetSelectedCell returns the contant of a cell for the currently selected row.
|
||||
func (v *Table) GetSelectedCell(col int) string {
|
||||
return TrimCell(v, v.selectedRow, col)
|
||||
// GetSelectedCell returns the content of a cell for the currently selected row.
|
||||
func (t *Table) GetSelectedCell(col int) string {
|
||||
return TrimCell(t, t.selectedRow, col)
|
||||
}
|
||||
|
||||
// GetSelectedRow fetch the currently selected row index.
|
||||
func (v *Table) GetSelectedRow() int {
|
||||
return v.selectedRow
|
||||
func (t *Table) GetSelectedRowIndex() int {
|
||||
return t.selectedRow
|
||||
}
|
||||
|
||||
// GetSelectedItem returns the currently selected item name.
|
||||
func (v *Table) GetSelectedItem() string {
|
||||
if v.selectedFn != nil {
|
||||
return v.selectedFn(v.selectedItem)
|
||||
func (t *Table) GetSelectedItem() string {
|
||||
if t.selectedFn != nil {
|
||||
return t.selectedFn(t.selectedItem)
|
||||
}
|
||||
return v.selectedItem
|
||||
return t.selectedItem
|
||||
}
|
||||
|
||||
// GetSelectedItems return currently marked or selected items names.
|
||||
func (v *Table) GetSelectedItems() []string {
|
||||
if len(v.marks) > 0 {
|
||||
func (t *Table) GetSelectedItems() []string {
|
||||
if len(t.marks) > 0 {
|
||||
var items []string
|
||||
for item, marked := range v.marks {
|
||||
for item, marked := range t.marks {
|
||||
if marked {
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
return []string{v.GetSelectedItem()}
|
||||
return []string{t.GetSelectedItem()}
|
||||
}
|
||||
|
||||
func (v *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
key := evt.Key()
|
||||
if key == tcell.KeyRune {
|
||||
if v.SearchBuff().IsActive() {
|
||||
v.SearchBuff().Add(evt.Rune())
|
||||
v.ClearSelection()
|
||||
v.doUpdate(v.filtered())
|
||||
v.UpdateTitle()
|
||||
v.SelectFirstRow()
|
||||
if t.SearchBuff().IsActive() {
|
||||
t.SearchBuff().Add(evt.Rune())
|
||||
t.ClearSelection()
|
||||
t.doUpdate(t.filtered())
|
||||
t.UpdateTitle()
|
||||
t.SelectFirstRow()
|
||||
return nil
|
||||
}
|
||||
key = asKey(evt)
|
||||
}
|
||||
|
||||
if a, ok := v.actions[key]; ok {
|
||||
if a, ok := t.actions[key]; ok {
|
||||
return a.Action(evt)
|
||||
}
|
||||
|
||||
|
|
@ -207,156 +216,156 @@ func (v *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
|
||||
// GetData fetch tabular data.
|
||||
func (v *Table) GetData() resource.TableData {
|
||||
return v.data
|
||||
func (t *Table) GetData() resource.TableData {
|
||||
return t.data
|
||||
}
|
||||
|
||||
// GetFilteredData fetch filtered tabular data.
|
||||
func (v *Table) GetFilteredData() resource.TableData {
|
||||
return v.filtered()
|
||||
func (t *Table) GetFilteredData() resource.TableData {
|
||||
return t.filtered()
|
||||
}
|
||||
|
||||
// SetBaseTitle set the table title.
|
||||
func (v *Table) SetBaseTitle(s string) {
|
||||
v.baseTitle = s
|
||||
func (t *Table) SetBaseTitle(s string) {
|
||||
t.baseTitle = s
|
||||
}
|
||||
|
||||
// GetBaseTitle fetch the current title.
|
||||
func (v *Table) GetBaseTitle() string {
|
||||
return v.baseTitle
|
||||
func (t *Table) GetBaseTitle() string {
|
||||
return t.baseTitle
|
||||
}
|
||||
|
||||
// SetColorerFn set the row colorer.
|
||||
func (v *Table) SetColorerFn(f ColorerFunc) {
|
||||
v.colorerFn = f
|
||||
func (t *Table) SetColorerFn(f ColorerFunc) {
|
||||
t.colorerFn = f
|
||||
}
|
||||
|
||||
// ActiveNS get the resource namespace.
|
||||
func (v *Table) ActiveNS() string {
|
||||
return v.activeNS
|
||||
func (t *Table) ActiveNS() string {
|
||||
return t.activeNS
|
||||
}
|
||||
|
||||
// SetActiveNS set the resource namespace.
|
||||
func (v *Table) SetActiveNS(ns string) {
|
||||
v.activeNS = ns
|
||||
func (t *Table) SetActiveNS(ns string) {
|
||||
t.activeNS = ns
|
||||
}
|
||||
|
||||
// SetSortCol sets in sort column index and order.
|
||||
func (v *Table) SetSortCol(index, count int, asc bool) {
|
||||
v.sortCol.index, v.sortCol.colCount, v.sortCol.asc = index, count, asc
|
||||
func (t *Table) SetSortCol(index, count int, asc bool) {
|
||||
t.sortCol.index, t.sortCol.colCount, t.sortCol.asc = index, count, asc
|
||||
}
|
||||
|
||||
// Update table content.
|
||||
func (v *Table) Update(data resource.TableData) {
|
||||
v.data = data
|
||||
if v.cmdBuff.Empty() {
|
||||
v.doUpdate(v.data)
|
||||
func (t *Table) Update(data resource.TableData) {
|
||||
t.data = data
|
||||
if t.cmdBuff.Empty() {
|
||||
t.doUpdate(t.data)
|
||||
} else {
|
||||
v.doUpdate(v.filtered())
|
||||
t.doUpdate(t.filtered())
|
||||
}
|
||||
v.UpdateTitle()
|
||||
v.updateSelection(true)
|
||||
t.UpdateTitle()
|
||||
t.updateSelection(true)
|
||||
}
|
||||
|
||||
func (v *Table) doUpdate(data resource.TableData) {
|
||||
v.activeNS = data.Namespace
|
||||
if v.activeNS == resource.AllNamespaces && v.activeNS != "*" {
|
||||
v.actions[KeyShiftP] = NewKeyAction("Sort Namespace", v.SortColCmd(-2), false)
|
||||
func (t *Table) doUpdate(data resource.TableData) {
|
||||
t.activeNS = data.Namespace
|
||||
if t.activeNS == resource.AllNamespaces && t.activeNS != "*" {
|
||||
t.actions[KeyShiftP] = NewKeyAction("Sort Namespace", t.SortColCmd(-2), false)
|
||||
} else {
|
||||
delete(v.actions, KeyShiftP)
|
||||
delete(t.actions, KeyShiftP)
|
||||
}
|
||||
v.Clear()
|
||||
t.Clear()
|
||||
|
||||
v.adjustSorter(data)
|
||||
t.adjustSorter(data)
|
||||
|
||||
var row int
|
||||
fg := config.AsColor(v.styles.Table().Header.FgColor)
|
||||
bg := config.AsColor(v.styles.Table().Header.BgColor)
|
||||
fg := config.AsColor(t.styles.Table().Header.FgColor)
|
||||
bg := config.AsColor(t.styles.Table().Header.BgColor)
|
||||
for col, h := range data.Header {
|
||||
v.AddHeaderCell(data.NumCols[h], col, h)
|
||||
c := v.GetCell(0, col)
|
||||
t.AddHeaderCell(data.NumCols[h], col, h)
|
||||
c := t.GetCell(0, col)
|
||||
c.SetBackgroundColor(bg)
|
||||
c.SetTextColor(fg)
|
||||
}
|
||||
row++
|
||||
|
||||
v.sort(data, row)
|
||||
t.sort(data, row)
|
||||
}
|
||||
|
||||
// SortColCmd designates a sorted column.
|
||||
func (v *Table) SortColCmd(col int) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
func (t *Table) SortColCmd(col int) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
v.sortCol.asc = true
|
||||
t.sortCol.asc = true
|
||||
switch col {
|
||||
case -2:
|
||||
v.sortCol.index = 0
|
||||
t.sortCol.index = 0
|
||||
case -1:
|
||||
v.sortCol.index = v.GetColumnCount() - 1
|
||||
t.sortCol.index = t.GetColumnCount() - 1
|
||||
default:
|
||||
v.sortCol.index = v.NameColIndex() + col
|
||||
t.sortCol.index = t.NameColIndex() + col
|
||||
|
||||
}
|
||||
v.Refresh()
|
||||
t.Refresh()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Table) adjustSorter(data resource.TableData) {
|
||||
func (t *Table) adjustSorter(data resource.TableData) {
|
||||
// Going from namespace to non namespace or vice-versa?
|
||||
switch {
|
||||
case v.sortCol.colCount == 0:
|
||||
case len(data.Header) > v.sortCol.colCount:
|
||||
v.sortCol.index++
|
||||
case len(data.Header) < v.sortCol.colCount:
|
||||
v.sortCol.index--
|
||||
case t.sortCol.colCount == 0:
|
||||
case len(data.Header) > t.sortCol.colCount:
|
||||
t.sortCol.index++
|
||||
case len(data.Header) < t.sortCol.colCount:
|
||||
t.sortCol.index--
|
||||
}
|
||||
v.sortCol.colCount = len(data.Header)
|
||||
if v.sortCol.index < 0 {
|
||||
v.sortCol.index = 0
|
||||
t.sortCol.colCount = len(data.Header)
|
||||
if t.sortCol.index < 0 {
|
||||
t.sortCol.index = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Table) sort(data resource.TableData, row int) {
|
||||
func (t *Table) sort(data resource.TableData, row int) {
|
||||
pads := make(MaxyPad, len(data.Header))
|
||||
ComputeMaxColumns(pads, v.sortCol.index, data)
|
||||
ComputeMaxColumns(pads, t.sortCol.index, data)
|
||||
|
||||
sortFn := defaultSort
|
||||
if v.sortFn != nil {
|
||||
sortFn = v.sortFn
|
||||
if t.sortFn != nil {
|
||||
sortFn = t.sortFn
|
||||
}
|
||||
prim, sec := sortAllRows(v.sortCol, data.Rows, sortFn)
|
||||
prim, sec := sortAllRows(t.sortCol, data.Rows, sortFn)
|
||||
for _, pk := range prim {
|
||||
for _, sk := range sec[pk] {
|
||||
v.buildRow(row, data, sk, pads)
|
||||
t.buildRow(row, data, sk, pads)
|
||||
row++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Table) buildRow(row int, data resource.TableData, sk string, pads MaxyPad) {
|
||||
func (t *Table) buildRow(row int, data resource.TableData, sk string, pads MaxyPad) {
|
||||
f := DefaultColorer
|
||||
if v.colorerFn != nil {
|
||||
f = v.colorerFn
|
||||
if t.colorerFn != nil {
|
||||
f = t.colorerFn
|
||||
}
|
||||
m := v.isMarked(sk)
|
||||
m := t.isMarked(sk)
|
||||
for col, field := range data.Rows[sk].Fields {
|
||||
header := data.Header[col]
|
||||
field, align := v.formatCell(data.NumCols[header], header, field+Deltas(data.Rows[sk].Deltas[col], field), pads[col])
|
||||
field, align := t.formatCell(data.NumCols[header], header, field+Deltas(data.Rows[sk].Deltas[col], field), pads[col])
|
||||
c := tview.NewTableCell(field)
|
||||
{
|
||||
c.SetExpansion(1)
|
||||
c.SetAlign(align)
|
||||
c.SetTextColor(f(data.Namespace, data.Rows[sk]))
|
||||
if m {
|
||||
c.SetBackgroundColor(config.AsColor(v.styles.Table().MarkColor))
|
||||
c.SetBackgroundColor(config.AsColor(t.styles.Table().MarkColor))
|
||||
}
|
||||
}
|
||||
v.SetCell(row, col, c)
|
||||
t.SetCell(row, col, c)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Table) formatCell(numerical bool, header, field string, padding int) (string, int) {
|
||||
func (t *Table) formatCell(numerical bool, header, field string, padding int) (string, int) {
|
||||
if header == "AGE" {
|
||||
dur, err := time.ParseDuration(field)
|
||||
if err == nil {
|
||||
|
|
@ -377,56 +386,56 @@ func (v *Table) formatCell(numerical bool, header, field string, padding int) (s
|
|||
}
|
||||
|
||||
// Refresh update the table data.
|
||||
func (v *Table) Refresh() {
|
||||
v.Update(v.data)
|
||||
func (t *Table) Refresh() {
|
||||
t.Update(t.data)
|
||||
}
|
||||
|
||||
// NameColIndex returns the index of the resource name column.
|
||||
func (v *Table) NameColIndex() int {
|
||||
func (t *Table) NameColIndex() int {
|
||||
col := 0
|
||||
if v.activeNS == resource.AllNamespaces {
|
||||
if t.activeNS == resource.AllNamespaces {
|
||||
col++
|
||||
}
|
||||
return col
|
||||
}
|
||||
|
||||
// AddHeaderCell configures a table cell header.
|
||||
func (v *Table) AddHeaderCell(numerical bool, col int, name string) {
|
||||
c := tview.NewTableCell(sortIndicator(v.sortCol, v.styles.Table(), col, name))
|
||||
func (t *Table) AddHeaderCell(numerical bool, col int, name string) {
|
||||
c := tview.NewTableCell(sortIndicator(t.sortCol, t.styles.Table(), col, name))
|
||||
c.SetExpansion(1)
|
||||
if numerical || cpuRX.MatchString(name) || memRX.MatchString(name) {
|
||||
c.SetAlign(tview.AlignRight)
|
||||
}
|
||||
v.SetCell(0, col, c)
|
||||
t.SetCell(0, col, c)
|
||||
}
|
||||
|
||||
func (v *Table) filtered() resource.TableData {
|
||||
if v.cmdBuff.Empty() || isLabelSelector(v.cmdBuff.String()) {
|
||||
return v.data
|
||||
func (t *Table) filtered() resource.TableData {
|
||||
if t.cmdBuff.Empty() || IsLabelSelector(t.cmdBuff.String()) {
|
||||
return t.data
|
||||
}
|
||||
|
||||
q := v.cmdBuff.String()
|
||||
q := t.cmdBuff.String()
|
||||
if isFuzzySelector(q) {
|
||||
return v.fuzzyFilter(q[2:])
|
||||
return t.fuzzyFilter(q[2:])
|
||||
}
|
||||
|
||||
return v.rxFilter(q)
|
||||
return t.rxFilter(q)
|
||||
}
|
||||
|
||||
func (v *Table) rxFilter(q string) resource.TableData {
|
||||
rx, err := regexp.Compile(`(?i)` + v.cmdBuff.String())
|
||||
func (t *Table) rxFilter(q string) resource.TableData {
|
||||
rx, err := regexp.Compile(`(?i)` + t.cmdBuff.String())
|
||||
if err != nil {
|
||||
log.Error().Err(errors.New("Invalid filter expression")).Msg("Regexp")
|
||||
v.cmdBuff.Clear()
|
||||
return v.data
|
||||
t.cmdBuff.Clear()
|
||||
return t.data
|
||||
}
|
||||
|
||||
filtered := resource.TableData{
|
||||
Header: v.data.Header,
|
||||
Header: t.data.Header,
|
||||
Rows: resource.RowEvents{},
|
||||
Namespace: v.data.Namespace,
|
||||
Namespace: t.data.Namespace,
|
||||
}
|
||||
for k, row := range v.data.Rows {
|
||||
for k, row := range t.data.Rows {
|
||||
f := strings.Join(row.Fields, " ")
|
||||
if rx.MatchString(f) {
|
||||
filtered.Rows[k] = row
|
||||
|
|
@ -436,123 +445,123 @@ func (v *Table) rxFilter(q string) resource.TableData {
|
|||
return filtered
|
||||
}
|
||||
|
||||
func (v *Table) fuzzyFilter(q string) resource.TableData {
|
||||
func (t *Table) fuzzyFilter(q string) resource.TableData {
|
||||
var ss, kk []string
|
||||
for k, row := range v.data.Rows {
|
||||
ss = append(ss, row.Fields[v.NameColIndex()])
|
||||
for k, row := range t.data.Rows {
|
||||
ss = append(ss, row.Fields[t.NameColIndex()])
|
||||
kk = append(kk, k)
|
||||
}
|
||||
|
||||
filtered := resource.TableData{
|
||||
Header: v.data.Header,
|
||||
Header: t.data.Header,
|
||||
Rows: resource.RowEvents{},
|
||||
Namespace: v.data.Namespace,
|
||||
Namespace: t.data.Namespace,
|
||||
}
|
||||
mm := fuzzy.Find(q, ss)
|
||||
for _, m := range mm {
|
||||
filtered.Rows[kk[m.Index]] = v.data.Rows[kk[m.Index]]
|
||||
filtered.Rows[kk[m.Index]] = t.data.Rows[kk[m.Index]]
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
// KeyBindings returns the bounded keys.
|
||||
func (v *Table) KeyBindings() KeyActions {
|
||||
return v.actions
|
||||
func (t *Table) KeyBindings() KeyActions {
|
||||
return t.actions
|
||||
}
|
||||
|
||||
// SearchBuff returns the associated command buffer.
|
||||
func (v *Table) SearchBuff() *CmdBuff {
|
||||
return v.cmdBuff
|
||||
func (t *Table) SearchBuff() *CmdBuff {
|
||||
return t.cmdBuff
|
||||
}
|
||||
|
||||
// ClearSelection reset selected row.
|
||||
func (v *Table) ClearSelection() {
|
||||
v.Select(0, 0)
|
||||
v.ScrollToBeginning()
|
||||
func (t *Table) ClearSelection() {
|
||||
t.Select(0, 0)
|
||||
t.ScrollToBeginning()
|
||||
}
|
||||
|
||||
// SelectFirstRow select first data row if any.
|
||||
func (v *Table) SelectFirstRow() {
|
||||
if v.GetRowCount() > 0 {
|
||||
v.Select(1, 0)
|
||||
func (t *Table) SelectFirstRow() {
|
||||
if t.GetRowCount() > 0 {
|
||||
t.Select(1, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// ShowDeleted marks row as deleted.
|
||||
func (v *Table) ShowDeleted() {
|
||||
r, _ := v.GetSelection()
|
||||
cols := v.GetColumnCount()
|
||||
func (t *Table) ShowDeleted() {
|
||||
r, _ := t.GetSelection()
|
||||
cols := t.GetColumnCount()
|
||||
for x := 0; x < cols; x++ {
|
||||
v.GetCell(r, x).SetAttributes(tcell.AttrDim)
|
||||
t.GetCell(r, x).SetAttributes(tcell.AttrDim)
|
||||
}
|
||||
}
|
||||
|
||||
// SetActions sets up keyboard action listener.
|
||||
func (v *Table) SetActions(aa KeyActions) {
|
||||
func (t *Table) AddActions(aa KeyActions) {
|
||||
for k, a := range aa {
|
||||
v.actions[k] = a
|
||||
t.actions[k] = a
|
||||
}
|
||||
}
|
||||
|
||||
// RmAction delete a keyed action.
|
||||
func (v *Table) RmAction(kk ...tcell.Key) {
|
||||
func (t *Table) RmAction(kk ...tcell.Key) {
|
||||
for _, k := range kk {
|
||||
delete(v.actions, k)
|
||||
delete(t.actions, k)
|
||||
}
|
||||
}
|
||||
|
||||
// Hints options
|
||||
func (v *Table) Hints() Hints {
|
||||
if v.actions != nil {
|
||||
return v.actions.Hints()
|
||||
func (t *Table) Hints() model.MenuHints {
|
||||
if t.actions != nil {
|
||||
return t.actions.Hints()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateTitle refreshes the table title.
|
||||
func (v *Table) UpdateTitle() {
|
||||
func (t *Table) UpdateTitle() {
|
||||
var title string
|
||||
|
||||
rc := v.GetRowCount()
|
||||
rc := t.GetRowCount()
|
||||
if rc > 0 {
|
||||
rc--
|
||||
}
|
||||
switch v.activeNS {
|
||||
switch t.activeNS {
|
||||
case resource.NotNamespaced, "*":
|
||||
title = skinTitle(fmt.Sprintf(titleFmt, v.baseTitle, rc), v.styles.Frame())
|
||||
title = skinTitle(fmt.Sprintf(titleFmt, t.baseTitle, rc), t.styles.Frame())
|
||||
default:
|
||||
ns := v.activeNS
|
||||
ns := t.activeNS
|
||||
if ns == resource.AllNamespaces {
|
||||
ns = resource.AllNamespace
|
||||
}
|
||||
title = skinTitle(fmt.Sprintf(nsTitleFmt, v.baseTitle, ns, rc), v.styles.Frame())
|
||||
title = skinTitle(fmt.Sprintf(nsTitleFmt, t.baseTitle, ns, rc), t.styles.Frame())
|
||||
}
|
||||
|
||||
if !v.cmdBuff.Empty() {
|
||||
cmd := v.cmdBuff.String()
|
||||
if isLabelSelector(cmd) {
|
||||
cmd = trimLabelSelector(cmd)
|
||||
if !t.cmdBuff.Empty() {
|
||||
cmd := t.cmdBuff.String()
|
||||
if IsLabelSelector(cmd) {
|
||||
cmd = TrimLabelSelector(cmd)
|
||||
}
|
||||
title += skinTitle(fmt.Sprintf(searchFmt, cmd), v.styles.Frame())
|
||||
title += skinTitle(fmt.Sprintf(SearchFmt, cmd), t.styles.Frame())
|
||||
}
|
||||
v.SetTitle(title)
|
||||
t.SetTitle(title)
|
||||
}
|
||||
|
||||
// SortInvertCmd reverses sorting order.
|
||||
func (v *Table) SortInvertCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
v.sortCol.asc = !v.sortCol.asc
|
||||
v.Refresh()
|
||||
func (t *Table) SortInvertCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
t.sortCol.asc = !t.sortCol.asc
|
||||
t.Refresh()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToggleMark toggles marked row
|
||||
func (v *Table) ToggleMark() {
|
||||
v.marks[v.GetSelectedItem()] = !v.marks[v.GetSelectedItem()]
|
||||
func (t *Table) ToggleMark() {
|
||||
t.marks[t.GetSelectedItem()] = !t.marks[t.GetSelectedItem()]
|
||||
}
|
||||
|
||||
func (v *Table) isMarked(item string) bool {
|
||||
return v.marks[item]
|
||||
func (t *Table) isMarked(item string) bool {
|
||||
return t.marks[item]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,25 +12,31 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
titleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[fg:bg:-]] "
|
||||
searchFmt = "<[filter:bg:r]/%s[fg:bg:-]> "
|
||||
nsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] "
|
||||
labelSelIndicator = "-l"
|
||||
descIndicator = "↓"
|
||||
ascIndicator = "↑"
|
||||
fullFmat = "%s-%s-%d.csv"
|
||||
noNSFmat = "%s-%d.csv"
|
||||
// SearchFmt represents a filter view title.
|
||||
SearchFmt = "<[filter:bg:r]/%s[fg:bg:-]> "
|
||||
|
||||
titleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[fg:bg:-]] "
|
||||
nsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] "
|
||||
descIndicator = "↓"
|
||||
ascIndicator = "↑"
|
||||
|
||||
// FullFmat specifies a namespaced dump file name.
|
||||
FullFmat = "%s-%s-%d.csv"
|
||||
|
||||
// NoNSFmat specifies a cluster wide dump file name.
|
||||
NoNSFmat = "%s-%d.csv"
|
||||
)
|
||||
|
||||
var (
|
||||
cpuRX = regexp.MustCompile(`\A.{0,1}CPU`)
|
||||
memRX = regexp.MustCompile(`\A.{0,1}MEM`)
|
||||
labelCmd = regexp.MustCompile(`\A\-l`)
|
||||
cpuRX = regexp.MustCompile(`\A.{0,1}CPU`)
|
||||
memRX = regexp.MustCompile(`\A.{0,1}MEM`)
|
||||
|
||||
// LabelCmd identifies a label query
|
||||
LabelCmd = regexp.MustCompile(`\A\-l`)
|
||||
|
||||
fuzzyCmd = regexp.MustCompile(`\A\-f`)
|
||||
)
|
||||
|
||||
type cleanseFn func(string) string
|
||||
|
||||
// TrimCell removes superfluous padding.
|
||||
func TrimCell(tv *Table, row, col int) string {
|
||||
c := tv.GetCell(row, col)
|
||||
|
|
@ -41,13 +47,15 @@ func TrimCell(tv *Table, row, col int) string {
|
|||
return strings.TrimSpace(c.Text)
|
||||
}
|
||||
|
||||
func isLabelSelector(s string) bool {
|
||||
// IsLabelSelector checks if query is a label query.
|
||||
func IsLabelSelector(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
return labelCmd.MatchString(s)
|
||||
return LabelCmd.MatchString(s)
|
||||
}
|
||||
|
||||
// IsFuzztySelector checks if query is fuzzy.
|
||||
func isFuzzySelector(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
|
|
@ -55,7 +63,8 @@ func isFuzzySelector(s string) bool {
|
|||
return fuzzyCmd.MatchString(s)
|
||||
}
|
||||
|
||||
func trimLabelSelector(s string) string {
|
||||
// TrimLabelSelector extracts label query.
|
||||
func TrimLabelSelector(s string) string {
|
||||
return strings.TrimSpace(s[2:])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsLabelSelector(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
sel string
|
||||
e bool
|
||||
}{
|
||||
"cool": {"-l app=fred,env=blee", true},
|
||||
"noMode": {"app=fred,env=blee", false},
|
||||
"noSpace": {"-lapp=fred,env=blee", true},
|
||||
"wrongLabel": {"-f app=fred,env=blee", false},
|
||||
}
|
||||
|
||||
for k, u := range uu {
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, IsLabelSelector(u.sel))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrimLabelSelector(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
sel, e string
|
||||
}{
|
||||
"cool": {"-l app=fred,env=blee", "app=fred,env=blee"},
|
||||
"noSpace": {"-lapp=fred,env=blee", "app=fred,env=blee"},
|
||||
}
|
||||
|
||||
for k, u := range uu {
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, TrimLabelSelector(u.sel))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTVSortRows(t *testing.T) {
|
||||
uu := []struct {
|
||||
rows resource.RowEvents
|
||||
col int
|
||||
asc bool
|
||||
first resource.Row
|
||||
e []string
|
||||
}{
|
||||
{
|
||||
resource.RowEvents{
|
||||
"row1": {Fields: resource.Row{"x", "y"}},
|
||||
"row2": {Fields: resource.Row{"a", "b"}},
|
||||
},
|
||||
0,
|
||||
true,
|
||||
resource.Row{"a", "b"},
|
||||
[]string{"row2", "row1"},
|
||||
},
|
||||
{
|
||||
resource.RowEvents{
|
||||
"row1": {Fields: resource.Row{"x", "y"}},
|
||||
"row2": {Fields: resource.Row{"a", "b"}},
|
||||
},
|
||||
1,
|
||||
true,
|
||||
resource.Row{"a", "b"},
|
||||
[]string{"row2", "row1"},
|
||||
},
|
||||
{
|
||||
resource.RowEvents{
|
||||
"row1": {Fields: resource.Row{"x", "y"}},
|
||||
"row2": {Fields: resource.Row{"a", "b"}},
|
||||
},
|
||||
1,
|
||||
false,
|
||||
resource.Row{"x", "y"},
|
||||
[]string{"row1", "row2"},
|
||||
},
|
||||
{
|
||||
resource.RowEvents{
|
||||
"row1": {Fields: resource.Row{"2175h48m0.06015s", "y"}},
|
||||
"row2": {Fields: resource.Row{"403h42m34.060166s", "b"}},
|
||||
},
|
||||
0,
|
||||
true,
|
||||
resource.Row{"403h42m34.060166s", "b"},
|
||||
[]string{"row2", "row1"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, u := range uu {
|
||||
keys := make([]string, len(u.rows))
|
||||
sortRows(u.rows, defaultSort, SortColumn{u.col, len(u.rows), u.asc}, keys)
|
||||
assert.Equal(t, u.e, keys)
|
||||
assert.Equal(t, u.first, u.rows[u.e[0]].Fields)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTableSortRows(b *testing.B) {
|
||||
evts := resource.RowEvents{
|
||||
"row1": {Fields: resource.Row{"x", "y"}},
|
||||
"row2": {Fields: resource.Row{"a", "b"}},
|
||||
}
|
||||
sc := SortColumn{0, 2, true}
|
||||
keys := make([]string, len(evts))
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
sortRows(evts, defaultSort, sc, keys)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +1,78 @@
|
|||
package ui
|
||||
package ui_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
func TestTVSortRows(t *testing.T) {
|
||||
uu := []struct {
|
||||
rows resource.RowEvents
|
||||
col int
|
||||
asc bool
|
||||
first resource.Row
|
||||
e []string
|
||||
}{
|
||||
{
|
||||
resource.RowEvents{
|
||||
"row1": {Fields: resource.Row{"x", "y"}},
|
||||
"row2": {Fields: resource.Row{"a", "b"}},
|
||||
},
|
||||
0,
|
||||
true,
|
||||
resource.Row{"a", "b"},
|
||||
[]string{"row2", "row1"},
|
||||
},
|
||||
{
|
||||
resource.RowEvents{
|
||||
"row1": {Fields: resource.Row{"x", "y"}},
|
||||
"row2": {Fields: resource.Row{"a", "b"}},
|
||||
},
|
||||
1,
|
||||
true,
|
||||
resource.Row{"a", "b"},
|
||||
[]string{"row2", "row1"},
|
||||
},
|
||||
{
|
||||
resource.RowEvents{
|
||||
"row1": {Fields: resource.Row{"x", "y"}},
|
||||
"row2": {Fields: resource.Row{"a", "b"}},
|
||||
},
|
||||
1,
|
||||
false,
|
||||
resource.Row{"x", "y"},
|
||||
[]string{"row1", "row2"},
|
||||
},
|
||||
{
|
||||
resource.RowEvents{
|
||||
"row1": {Fields: resource.Row{"2175h48m0.06015s", "y"}},
|
||||
"row2": {Fields: resource.Row{"403h42m34.060166s", "b"}},
|
||||
},
|
||||
0,
|
||||
true,
|
||||
resource.Row{"403h42m34.060166s", "b"},
|
||||
[]string{"row2", "row1"},
|
||||
},
|
||||
}
|
||||
func TestTableNew(t *testing.T) {
|
||||
v := ui.NewTable("fred")
|
||||
s, _ := config.NewStyles("")
|
||||
ctx := context.WithValue(context.Background(), ui.KeyStyles, s)
|
||||
v.Init(ctx)
|
||||
|
||||
assert.Equal(t, "fred", v.GetBaseTitle())
|
||||
|
||||
v.SetBaseTitle("bozo")
|
||||
assert.Equal(t, "bozo", v.GetBaseTitle())
|
||||
|
||||
for _, u := range uu {
|
||||
keys := make([]string, len(u.rows))
|
||||
sortRows(u.rows, defaultSort, SortColumn{u.col, len(u.rows), u.asc}, keys)
|
||||
assert.Equal(t, u.e, keys)
|
||||
assert.Equal(t, u.first, u.rows[u.e[0]].Fields)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTableSortRows(b *testing.B) {
|
||||
evts := resource.RowEvents{
|
||||
"row1": {Fields: resource.Row{"x", "y"}},
|
||||
"row2": {Fields: resource.Row{"a", "b"}},
|
||||
}
|
||||
sc := SortColumn{0, 2, true}
|
||||
keys := make([]string, len(evts))
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
sortRows(evts, defaultSort, sc, keys)
|
||||
func TestTableUpdate(t *testing.T) {
|
||||
v := ui.NewTable("fred")
|
||||
s, _ := config.NewStyles("")
|
||||
ctx := context.WithValue(context.Background(), ui.KeyStyles, s)
|
||||
v.Init(ctx)
|
||||
|
||||
v.Update(makeTableData())
|
||||
|
||||
assert.Equal(t, 3, v.GetRowCount())
|
||||
assert.Equal(t, 3, v.GetColumnCount())
|
||||
}
|
||||
|
||||
func TestTableSelection(t *testing.T) {
|
||||
v := ui.NewTable("fred")
|
||||
s, _ := config.NewStyles("")
|
||||
ctx := context.WithValue(context.Background(), ui.KeyStyles, s)
|
||||
v.Init(ctx)
|
||||
|
||||
v.Update(makeTableData())
|
||||
|
||||
v.SelectRow(1, true)
|
||||
assert.True(t, v.RowSelected())
|
||||
assert.Equal(t, resource.Row{"blee", "duh", "fred"}, v.GetRow())
|
||||
assert.Equal(t, "blee", v.GetSelectedCell(0))
|
||||
assert.Equal(t, 1, v.GetSelectedRowIndex())
|
||||
assert.Equal(t, []string{"blee/duh"}, v.GetSelectedItems())
|
||||
|
||||
v.ClearSelection()
|
||||
v.SelectFirstRow()
|
||||
assert.Equal(t, 1, v.GetSelectedRowIndex())
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
||||
func makeTableData() resource.TableData {
|
||||
return resource.TableData{
|
||||
Namespace: "",
|
||||
Header: resource.Row{"a", "b", "c"},
|
||||
Rows: resource.RowEvents{
|
||||
"r1": &resource.RowEvent{
|
||||
Action: watch.Added,
|
||||
Fields: resource.Row{"blee", "duh", "fred"},
|
||||
Deltas: resource.Row{"", "", ""},
|
||||
},
|
||||
"r2": &resource.RowEvent{
|
||||
Action: watch.Added,
|
||||
Fields: resource.Row{"fred", "duh", "zorg"},
|
||||
Deltas: resource.Row{"", "", ""},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
const (
|
||||
aliasTitle = "Aliases"
|
||||
aliasTitleFmt = " [mediumseagreen::b]%s([fuchsia::b]%d[fuchsia::-][mediumseagreen::-]) "
|
||||
)
|
||||
|
||||
// Alias represents a command alias view.
|
||||
type Alias struct {
|
||||
*Table
|
||||
}
|
||||
|
||||
// NewAlias returns a new alias view.
|
||||
func NewAlias() *Alias {
|
||||
return &Alias{
|
||||
Table: NewTable(aliasTitle),
|
||||
}
|
||||
}
|
||||
|
||||
// Init the view.
|
||||
func (a *Alias) Init(ctx context.Context) {
|
||||
a.Table.Init(ctx)
|
||||
|
||||
a.SetBorderFocusColor(tcell.ColorMediumSpringGreen)
|
||||
a.SetSelectedStyle(tcell.ColorWhite, tcell.ColorMediumSpringGreen, tcell.AttrNone)
|
||||
a.SetColorerFn(aliasColorer)
|
||||
a.SetActiveNS("")
|
||||
a.registerActions()
|
||||
|
||||
a.Update(a.hydrate())
|
||||
a.resetTitle()
|
||||
}
|
||||
|
||||
func (a *Alias) Name() string {
|
||||
return aliasTitle
|
||||
}
|
||||
|
||||
func (a *Alias) Start() {}
|
||||
func (a *Alias) Stop() {}
|
||||
|
||||
func (a *Alias) registerActions() {
|
||||
a.RmAction(ui.KeyShiftA)
|
||||
a.RmAction(ui.KeyShiftN)
|
||||
a.RmAction(tcell.KeyCtrlS)
|
||||
|
||||
a.AddActions(ui.KeyActions{
|
||||
tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, true),
|
||||
tcell.KeyEscape: ui.NewKeyAction("Reset", a.resetCmd, false),
|
||||
ui.KeySlash: ui.NewKeyAction("Filter", a.activateCmd, false),
|
||||
ui.KeyShiftR: ui.NewKeyAction("Sort Resource", a.SortColCmd(0), false),
|
||||
ui.KeyShiftC: ui.NewKeyAction("Sort Command", a.SortColCmd(1), false),
|
||||
ui.KeyShiftA: ui.NewKeyAction("Sort ApiGroup", a.SortColCmd(2), false),
|
||||
})
|
||||
}
|
||||
|
||||
func (a *Alias) getTitle() string {
|
||||
return aliasTitle
|
||||
}
|
||||
|
||||
func (a *Alias) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !a.SearchBuff().Empty() {
|
||||
a.SearchBuff().Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
return a.backCmd(evt)
|
||||
}
|
||||
|
||||
func (a *Alias) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
r, _ := a.GetSelection()
|
||||
if r != 0 {
|
||||
s := ui.TrimCell(a.Table.Table, r, 1)
|
||||
tokens := strings.Split(s, ",")
|
||||
a.app.gotoResource(tokens[0], true)
|
||||
return nil
|
||||
}
|
||||
|
||||
if a.SearchBuff().IsActive() {
|
||||
return a.activateCmd(evt)
|
||||
}
|
||||
|
||||
return evt
|
||||
}
|
||||
|
||||
func (a *Alias) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if a.SearchBuff().IsActive() {
|
||||
a.SearchBuff().Reset()
|
||||
} else {
|
||||
a.app.Content.Pop()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Alias) hydrate() resource.TableData {
|
||||
data := resource.TableData{
|
||||
Header: resource.Row{"RESOURCE", "COMMAND", "APIGROUP"},
|
||||
Rows: make(resource.RowEvents, len(aliases.Alias)),
|
||||
Namespace: resource.NotNamespaced,
|
||||
}
|
||||
|
||||
aa := make(map[string][]string, len(aliases.Alias))
|
||||
for alias, gvr := range aliases.Alias {
|
||||
if _, ok := aa[gvr]; ok {
|
||||
aa[gvr] = append(aa[gvr], alias)
|
||||
} else {
|
||||
aa[gvr] = []string{alias}
|
||||
}
|
||||
}
|
||||
|
||||
for gvr, aliases := range aa {
|
||||
g := k8s.GVR(gvr)
|
||||
fields := resource.Row{
|
||||
ui.Pad(g.ToR(), 30),
|
||||
ui.Pad(strings.Join(aliases, ","), 70),
|
||||
ui.Pad(g.ToG(), 30),
|
||||
}
|
||||
data.Rows[string(gvr)] = &resource.RowEvent{
|
||||
Action: resource.New,
|
||||
Fields: fields,
|
||||
Deltas: fields,
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func (a *Alias) resetTitle() {
|
||||
a.SetTitle(fmt.Sprintf(aliasTitleFmt, aliasTitle, a.GetRowCount()-1))
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package view_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/k9s/internal/view"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func TestAliasNew(t *testing.T) {
|
||||
v := view.NewAlias()
|
||||
v.Init(makeContext())
|
||||
|
||||
assert.Equal(t, 3, v.GetColumnCount())
|
||||
assert.Equal(t, 16, v.GetRowCount())
|
||||
assert.Equal(t, "Aliases", v.Name())
|
||||
assert.Equal(t, 10, len(v.Hints()))
|
||||
}
|
||||
|
||||
func TestAliasSearch(t *testing.T) {
|
||||
v := view.NewAlias()
|
||||
v.Init(makeContext())
|
||||
v.SearchBuff().SetActive(true)
|
||||
v.SearchBuff().Set("dump")
|
||||
|
||||
v.SendKey(tcell.NewEventKey(tcell.KeyRune, 'd', tcell.ModNone))
|
||||
|
||||
assert.Equal(t, 3, v.GetColumnCount())
|
||||
assert.Equal(t, 1, v.GetRowCount())
|
||||
}
|
||||
|
||||
func TestAliasGoto(t *testing.T) {
|
||||
v := view.NewAlias()
|
||||
v.Init(makeContext())
|
||||
v.Select(0, 0)
|
||||
|
||||
b := buffL{}
|
||||
v.SearchBuff().SetActive(true)
|
||||
v.SearchBuff().AddListener(&b)
|
||||
v.SendKey(tcell.NewEventKey(tcell.KeyEnter, 256, tcell.ModNone))
|
||||
|
||||
assert.True(t, v.SearchBuff().IsActive())
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
||||
type buffL struct {
|
||||
active int
|
||||
changed int
|
||||
}
|
||||
|
||||
func (b *buffL) BufferChanged(s string) {
|
||||
b.changed++
|
||||
}
|
||||
func (b *buffL) BufferActive(state bool, kind ui.BufferKind) {
|
||||
b.active++
|
||||
}
|
||||
|
||||
func makeContext() context.Context {
|
||||
a := view.NewApp(config.NewConfig(ks{}))
|
||||
ctx := context.WithValue(context.Background(), ui.KeyApp, a)
|
||||
return context.WithValue(ctx, ui.KeyStyles, a.Styles)
|
||||
}
|
||||
|
||||
type ks struct{}
|
||||
|
||||
func (k ks) CurrentContextName() (string, error) {
|
||||
return "test", nil
|
||||
}
|
||||
|
||||
func (k ks) CurrentClusterName() (string, error) {
|
||||
return "test", nil
|
||||
}
|
||||
|
||||
func (k ks) CurrentNamespaceName() (string, error) {
|
||||
return "test", nil
|
||||
}
|
||||
|
||||
func (k ks) ClusterNames() ([]string, error) {
|
||||
return []string{"test"}, nil
|
||||
}
|
||||
|
||||
func (k ks) NamespaceNames(nn []v1.Namespace) []string {
|
||||
return []string{"test"}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package views
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/k9s/internal/watch"
|
||||
|
|
@ -23,47 +24,50 @@ const (
|
|||
indicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%"
|
||||
)
|
||||
|
||||
type (
|
||||
focusHandler func(tview.Primitive)
|
||||
// ActionsFunc augments Keybindinga.
|
||||
type ActionsFunc func(ui.KeyActions)
|
||||
|
||||
forwarder interface {
|
||||
Start(path, co string, ports []string) (*portforward.PortForwarder, error)
|
||||
Stop()
|
||||
Path() string
|
||||
Container() string
|
||||
Ports() []string
|
||||
Active() bool
|
||||
Age() string
|
||||
}
|
||||
type focusHandler func(tview.Primitive)
|
||||
|
||||
resourceViewer interface {
|
||||
ui.Igniter
|
||||
type forwarder interface {
|
||||
Start(path, co string, ports []string) (*portforward.PortForwarder, error)
|
||||
Stop()
|
||||
Path() string
|
||||
Container() string
|
||||
Ports() []string
|
||||
Active() bool
|
||||
Age() string
|
||||
}
|
||||
|
||||
setEnterFn(enterFn)
|
||||
setColorerFn(ui.ColorerFunc)
|
||||
setDecorateFn(decorateFn)
|
||||
setExtraActionsFn(ui.ActionsFunc)
|
||||
masterPage() *tableView
|
||||
}
|
||||
// ResourceViewer represents a generic resource viewer.
|
||||
type ResourceViewer interface {
|
||||
model.Component
|
||||
|
||||
appView struct {
|
||||
*ui.App
|
||||
setEnterFn(enterFn)
|
||||
setColorerFn(ui.ColorerFunc)
|
||||
setDecorateFn(decorateFn)
|
||||
setExtraActionsFn(ActionsFunc)
|
||||
masterPage() *Table
|
||||
}
|
||||
|
||||
command *command
|
||||
cancel context.CancelFunc
|
||||
informer *watch.Informer
|
||||
stopCh chan struct{}
|
||||
forwarders map[string]forwarder
|
||||
version string
|
||||
showHeader bool
|
||||
filter string
|
||||
}
|
||||
)
|
||||
// App represents an application view.
|
||||
type App struct {
|
||||
*ui.App
|
||||
|
||||
Content *PageStack
|
||||
command *command
|
||||
informer *watch.Informer
|
||||
stopCh chan struct{}
|
||||
forwarders map[string]forwarder
|
||||
version string
|
||||
showHeader bool
|
||||
}
|
||||
|
||||
// NewApp returns a K9s app instance.
|
||||
func NewApp(cfg *config.Config) *appView {
|
||||
v := appView{
|
||||
func NewApp(cfg *config.Config) *App {
|
||||
v := App{
|
||||
App: ui.NewApp(),
|
||||
Content: NewPageStack(),
|
||||
forwarders: make(map[string]forwarder),
|
||||
}
|
||||
v.Config = cfg
|
||||
|
|
@ -71,17 +75,30 @@ func NewApp(cfg *config.Config) *appView {
|
|||
v.command = newCommand(&v)
|
||||
|
||||
v.Views()["indicator"] = ui.NewIndicatorView(v.App, v.Styles)
|
||||
v.Views()["flash"] = ui.NewFlashView(v.Application, "Initializing...")
|
||||
v.Views()["clusterInfo"] = newClusterInfoView(&v, k8s.NewMetricsServer(cfg.GetConnection()))
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
func (a *appView) Init(version string, rate int) {
|
||||
// ActiveView returns the currently active view.
|
||||
func (a *App) ActiveView() model.Component {
|
||||
return a.Content.GetPrimitive("main").(model.Component)
|
||||
}
|
||||
|
||||
func (a *App) PrevCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
a.Content.Pop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) Init(version string, rate int) {
|
||||
ctx := context.WithValue(context.Background(), ui.KeyApp, a)
|
||||
a.Content.Init(ctx)
|
||||
a.Content.Stack.AddListener(a.Crumbs())
|
||||
|
||||
a.version = version
|
||||
a.CmdBuff().AddListener(a)
|
||||
a.App.Init()
|
||||
|
||||
a.AddActions(ui.KeyActions{
|
||||
ui.KeyH: ui.NewKeyAction("ToggleHeader", a.toggleHeaderCmd, false),
|
||||
ui.KeyHelp: ui.NewKeyAction("Help", a.helpCmd, false),
|
||||
|
|
@ -101,24 +118,52 @@ func (a *appView) Init(version string, rate int) {
|
|||
}
|
||||
}
|
||||
|
||||
main := tview.NewFlex()
|
||||
main.SetDirection(tview.FlexRow)
|
||||
a.Main().AddPage("main", main, true, false)
|
||||
a.Main().AddPage("splash", ui.NewSplash(a.Styles, version), true, true)
|
||||
main := tview.NewFlex().SetDirection(tview.FlexRow)
|
||||
a.Main.AddPage("main", main, true, false)
|
||||
a.Main.AddPage("splash", ui.NewSplash(a.Styles, version), true, true)
|
||||
|
||||
// ctx := context.WithValue(context.Background(), ui.KeyApp, a)
|
||||
// a.Content.Init(ctx)
|
||||
// d := NewDetails(a, nil)
|
||||
// d.SetText("Fuck!!")
|
||||
// a.Content.Push(d)
|
||||
// d = NewDetails(a, nil)
|
||||
// d.SetText("Shit!!")
|
||||
// a.Content.Push(d)
|
||||
|
||||
main.AddItem(a.indicator(), 1, 1, false)
|
||||
main.AddItem(a.Frame(), 0, 10, true)
|
||||
main.AddItem(a.Content, 0, 10, true)
|
||||
main.AddItem(a.Crumbs(), 2, 1, false)
|
||||
main.AddItem(a.Flash(), 2, 1, false)
|
||||
a.toggleHeader(!a.Config.K9s.GetHeadless())
|
||||
}
|
||||
|
||||
// func (a *App) StackPushed(c model.Component) {
|
||||
// ctx := context.WithValue(context.Background(), ui.KeyApp, a)
|
||||
// ctx, a.cancelFn = context.WithCancel(context.Background())
|
||||
// c.Init(ctx)
|
||||
|
||||
// a.Frame().AddPage(c.Name(), c, true, true)
|
||||
// a.SetFocus(c)
|
||||
// a.setHints(c.Hints())
|
||||
// }
|
||||
|
||||
// func (a *App) StackPopped(o, c model.Component) {
|
||||
// a.Frame().RemovePage(o.Name())
|
||||
// if c != nil {
|
||||
// a.StackPushed(c)
|
||||
// }
|
||||
// }
|
||||
|
||||
// func (a *App) StackTop(model.Component) {
|
||||
// }
|
||||
|
||||
// Changed indicates the buffer was changed.
|
||||
func (a *appView) BufferChanged(s string) {}
|
||||
func (a *App) BufferChanged(s string) {}
|
||||
|
||||
// Active indicates the buff activity changed.
|
||||
func (a *appView) BufferActive(state bool, _ ui.BufferKind) {
|
||||
flex, ok := a.Main().GetPrimitive("main").(*tview.Flex)
|
||||
func (a *App) BufferActive(state bool, _ ui.BufferKind) {
|
||||
flex, ok := a.Main.GetPrimitive("main").(*tview.Flex)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
|
@ -130,9 +175,9 @@ func (a *appView) BufferActive(state bool, _ ui.BufferKind) {
|
|||
a.Draw()
|
||||
}
|
||||
|
||||
func (a *appView) toggleHeader(flag bool) {
|
||||
func (a *App) toggleHeader(flag bool) {
|
||||
a.showHeader = flag
|
||||
flex := a.Main().GetPrimitive("main").(*tview.Flex)
|
||||
flex := a.Main.GetPrimitive("main").(*tview.Flex)
|
||||
if a.showHeader {
|
||||
flex.RemoveItemAtIndex(0)
|
||||
flex.AddItemAtIndex(0, a.buildHeader(), 7, 1, false)
|
||||
|
|
@ -143,7 +188,7 @@ func (a *appView) toggleHeader(flag bool) {
|
|||
}
|
||||
}
|
||||
|
||||
func (a *appView) buildHeader() tview.Primitive {
|
||||
func (a *App) buildHeader() tview.Primitive {
|
||||
header := tview.NewFlex()
|
||||
header.SetBorderPadding(0, 0, 1, 1)
|
||||
header.SetDirection(tview.FlexColumn)
|
||||
|
|
@ -157,7 +202,7 @@ func (a *appView) buildHeader() tview.Primitive {
|
|||
return header
|
||||
}
|
||||
|
||||
func (a *appView) clusterUpdater(ctx context.Context) {
|
||||
func (a *App) clusterUpdater(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
|
@ -175,7 +220,7 @@ func (a *appView) clusterUpdater(ctx context.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func (a *appView) refreshIndicator() {
|
||||
func (a *App) refreshIndicator() {
|
||||
mx := k8s.NewMetricsServer(a.Conn())
|
||||
cluster := resource.NewCluster(a.Conn(), &log.Logger, mx)
|
||||
var cmx k8s.ClusterMetrics
|
||||
|
|
@ -205,7 +250,7 @@ func (a *appView) refreshIndicator() {
|
|||
a.indicator().SetPermanent(info)
|
||||
}
|
||||
|
||||
func (a *appView) switchNS(ns string) bool {
|
||||
func (a *App) switchNS(ns string) bool {
|
||||
if ns == resource.AllNamespace {
|
||||
ns = resource.AllNamespaces
|
||||
}
|
||||
|
|
@ -218,7 +263,7 @@ func (a *appView) switchNS(ns string) bool {
|
|||
return a.startInformer(ns)
|
||||
}
|
||||
|
||||
func (a *appView) switchCtx(ctx string, load bool) error {
|
||||
func (a *App) switchCtx(ctx string, load bool) error {
|
||||
l := resource.NewContext(a.Conn())
|
||||
if err := l.Switch(ctx); err != nil {
|
||||
return err
|
||||
|
|
@ -240,7 +285,7 @@ func (a *appView) switchCtx(ctx string, load bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (a *appView) startInformer(ns string) bool {
|
||||
func (a *App) startInformer(ns string) bool {
|
||||
if a.stopCh != nil {
|
||||
close(a.stopCh)
|
||||
a.stopCh = nil
|
||||
|
|
@ -263,21 +308,18 @@ func (a *appView) startInformer(ns string) bool {
|
|||
}
|
||||
|
||||
// BailOut exists the application.
|
||||
func (a *appView) BailOut() {
|
||||
func (a *App) BailOut() {
|
||||
if a.stopCh != nil {
|
||||
log.Debug().Msg("<<<< Stopping Watcher")
|
||||
close(a.stopCh)
|
||||
a.stopCh = nil
|
||||
}
|
||||
|
||||
if a.cancel != nil {
|
||||
a.cancel()
|
||||
}
|
||||
a.stopForwarders()
|
||||
a.App.BailOut()
|
||||
}
|
||||
|
||||
func (a *appView) stopForwarders() {
|
||||
func (a *App) stopForwarders() {
|
||||
for k, f := range a.forwarders {
|
||||
log.Debug().Msgf("Deleting forwarder %s", f.Path())
|
||||
f.Stop()
|
||||
|
|
@ -286,7 +328,7 @@ func (a *appView) stopForwarders() {
|
|||
}
|
||||
|
||||
// Run starts the application loop
|
||||
func (a *appView) Run() {
|
||||
func (a *App) Run() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
go a.clusterUpdater(ctx)
|
||||
|
|
@ -301,7 +343,7 @@ func (a *appView) Run() {
|
|||
go func() {
|
||||
<-time.After(splashTime * time.Second)
|
||||
a.QueueUpdateDraw(func() {
|
||||
a.Main().SwitchToPage("main")
|
||||
a.Main.SwitchToPage("main")
|
||||
})
|
||||
}()
|
||||
|
||||
|
|
@ -311,7 +353,7 @@ func (a *appView) Run() {
|
|||
}
|
||||
}
|
||||
|
||||
func (a *appView) status(l ui.FlashLevel, msg string) {
|
||||
func (a *App) status(l ui.FlashLevel, msg string) {
|
||||
a.Flash().Info(msg)
|
||||
if a.Config.K9s.GetHeadless() {
|
||||
a.setIndicator(l, msg)
|
||||
|
|
@ -321,7 +363,7 @@ func (a *appView) status(l ui.FlashLevel, msg string) {
|
|||
a.Draw()
|
||||
}
|
||||
|
||||
func (a *appView) setLogo(l ui.FlashLevel, msg string) {
|
||||
func (a *App) setLogo(l ui.FlashLevel, msg string) {
|
||||
switch l {
|
||||
case ui.FlashErr:
|
||||
a.Logo().Err(msg)
|
||||
|
|
@ -335,7 +377,7 @@ func (a *appView) setLogo(l ui.FlashLevel, msg string) {
|
|||
a.Draw()
|
||||
}
|
||||
|
||||
func (a *appView) setIndicator(l ui.FlashLevel, msg string) {
|
||||
func (a *App) setIndicator(l ui.FlashLevel, msg string) {
|
||||
switch l {
|
||||
case ui.FlashErr:
|
||||
a.indicator().Err(msg)
|
||||
|
|
@ -349,7 +391,7 @@ func (a *appView) setIndicator(l ui.FlashLevel, msg string) {
|
|||
a.Draw()
|
||||
}
|
||||
|
||||
func (a *appView) toggleHeaderCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
func (a *App) toggleHeaderCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if a.Cmd().InCmdMode() {
|
||||
return evt
|
||||
}
|
||||
|
|
@ -361,16 +403,7 @@ func (a *appView) toggleHeaderCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (a *appView) prevCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if top, ok := a.command.previousCmd(); ok {
|
||||
log.Debug().Msgf("Previous command %s", top)
|
||||
a.gotoResource(top, false)
|
||||
return nil
|
||||
}
|
||||
return evt
|
||||
}
|
||||
|
||||
func (a *appView) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
func (a *App) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if a.CmdBuff().IsActive() && !a.CmdBuff().Empty() {
|
||||
a.gotoResource(a.GetCmd(), true)
|
||||
a.ResetCmd()
|
||||
|
|
@ -381,53 +414,37 @@ func (a *appView) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
return evt
|
||||
}
|
||||
|
||||
func (a *appView) helpCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if _, ok := a.Frame().GetPrimitive("main").(*helpView); ok {
|
||||
func (a *App) helpCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if _, ok := a.Content.GetPrimitive("main").(*Help); ok {
|
||||
return evt
|
||||
}
|
||||
h := newHelpView(a, a.ActiveView(), a.GetHints())
|
||||
a.inject(h)
|
||||
a.inject(NewHelp())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *appView) aliasCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if _, ok := a.Frame().GetPrimitive("main").(*aliasView); ok {
|
||||
func (a *App) aliasCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if _, ok := a.Content.GetPrimitive("main").(*Alias); ok {
|
||||
return evt
|
||||
}
|
||||
a.inject(newAliasView(a, a.ActiveView()))
|
||||
a.inject(NewAlias())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *appView) gotoResource(res string, record bool) bool {
|
||||
if a.cancel != nil {
|
||||
a.cancel()
|
||||
}
|
||||
valid := a.command.run(res)
|
||||
if valid && record {
|
||||
a.command.pushCmd(res)
|
||||
}
|
||||
|
||||
return valid
|
||||
func (a *App) gotoResource(res string, record bool) bool {
|
||||
return a.command.run(res)
|
||||
}
|
||||
|
||||
func (a *appView) inject(i ui.Igniter) {
|
||||
if a.cancel != nil {
|
||||
a.cancel()
|
||||
}
|
||||
a.Frame().RemovePage("main")
|
||||
var ctx context.Context
|
||||
ctx, a.cancel = context.WithCancel(context.Background())
|
||||
i.Init(ctx, a.Config.ActiveNamespace())
|
||||
a.Frame().AddPage("main", i, true, true)
|
||||
a.SetFocus(i)
|
||||
func (a *App) inject(c model.Component) {
|
||||
log.Debug().Msgf("Injecting component %#v", c)
|
||||
a.Content.Push(c)
|
||||
}
|
||||
|
||||
func (a *appView) clusterInfo() *clusterInfoView {
|
||||
func (a *App) clusterInfo() *clusterInfoView {
|
||||
return a.Views()["clusterInfo"].(*clusterInfoView)
|
||||
}
|
||||
|
||||
func (a *appView) indicator() *ui.IndicatorView {
|
||||
func (a *App) indicator() *ui.IndicatorView {
|
||||
return a.Views()["indicator"].(*ui.IndicatorView)
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package view_test
|
||||
|
||||
// import (
|
||||
// "testing"
|
||||
|
||||
// "github.com/derailed/k9s/internal/config"
|
||||
// "github.com/derailed/k9s/internal/view"
|
||||
// "github.com/stretchr/testify/assert"
|
||||
// )
|
||||
|
||||
// func TestAppNew(t *testing.T) {
|
||||
// a := view.NewApp(config.NewConfig(ks{}))
|
||||
// a.Init("blee", 10)
|
||||
|
||||
// assert.Equal(t, 11, len(a.GetActions()))
|
||||
// assert.Equal(t, false, a.HasSkins)
|
||||
// }
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package views
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/perf"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
|
|
@ -34,72 +35,86 @@ var (
|
|||
benchHeader = resource.Row{"NAMESPACE", "NAME", "STATUS", "TIME", "REQ/S", "2XX", "4XX/5XX", "REPORT", "AGE"}
|
||||
)
|
||||
|
||||
type benchView struct {
|
||||
*masterDetail
|
||||
// Bench represents a service benchmark results view.
|
||||
type Bench struct {
|
||||
*MasterDetail
|
||||
|
||||
app *appView
|
||||
cancelFn context.CancelFunc
|
||||
}
|
||||
|
||||
func newBenchView(title, gvr string, app *appView, _ resource.List) resourceViewer {
|
||||
v := benchView{app: app}
|
||||
v.masterDetail = newMasterDetail(benchTitle, "", app, v.backCmd)
|
||||
v.keyBindings()
|
||||
|
||||
return &v
|
||||
func NewBench(title, gvr string, _ resource.List) ResourceViewer {
|
||||
return &Bench{
|
||||
MasterDetail: NewMasterDetail(),
|
||||
}
|
||||
}
|
||||
|
||||
// Init the view.
|
||||
func (v *benchView) Init(ctx context.Context, ns string) {
|
||||
v.masterDetail.init(ctx, ns)
|
||||
func (b *Bench) Init(ctx context.Context) {
|
||||
b.MasterDetail.Init(ctx)
|
||||
b.keyBindings()
|
||||
|
||||
tv := v.masterPage()
|
||||
tv := b.masterPage()
|
||||
tv.SetBorderFocusColor(tcell.ColorSeaGreen)
|
||||
tv.SetSelectedStyle(tcell.ColorWhite, tcell.ColorSeaGreen, tcell.AttrNone)
|
||||
tv.SetColorerFn(benchColorer)
|
||||
|
||||
dv := v.detailsPage()
|
||||
dv := b.detailsPage()
|
||||
dv.setCategory("Bench")
|
||||
dv.SetTextColor(tcell.ColorSeaGreen)
|
||||
|
||||
if err := v.watchBenchDir(ctx); err != nil {
|
||||
v.app.Flash().Errf("Unable to watch benchmarks directory %s", err)
|
||||
}
|
||||
|
||||
v.refresh()
|
||||
b.Start()
|
||||
b.refresh()
|
||||
tv.SetSortCol(tv.NameColIndex()+7, 0, true)
|
||||
tv.Refresh()
|
||||
tv.Select(1, 0)
|
||||
v.app.SetFocus(tv)
|
||||
v.app.SetHints(tv.Hints())
|
||||
}
|
||||
|
||||
func (v *benchView) setEnterFn(enterFn) {}
|
||||
func (v *benchView) setColorerFn(ui.ColorerFunc) {}
|
||||
func (v *benchView) setDecorateFn(decorateFn) {}
|
||||
func (v *benchView) setExtraActionsFn(ui.ActionsFunc) {}
|
||||
func (b *Bench) Start() {
|
||||
var ctx context.Context
|
||||
|
||||
func (v *benchView) refresh() {
|
||||
tv := v.masterPage()
|
||||
tv.Update(v.hydrate())
|
||||
ctx, b.cancelFn = context.WithCancel(context.Background())
|
||||
if err := b.watchBenchDir(ctx); err != nil {
|
||||
b.app.Flash().Errf("Unable to watch benchmarks directory %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bench) Stop() {
|
||||
if b.cancelFn != nil {
|
||||
b.cancelFn()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bench) Name() string {
|
||||
return "benchmarks"
|
||||
}
|
||||
|
||||
func (b *Bench) setEnterFn(enterFn) {}
|
||||
func (b *Bench) setColorerFn(ui.ColorerFunc) {}
|
||||
func (b *Bench) setDecorateFn(decorateFn) {}
|
||||
func (b *Bench) setExtraActionsFn(ActionsFunc) {}
|
||||
|
||||
func (b *Bench) refresh() {
|
||||
tv := b.masterPage()
|
||||
tv.Update(b.hydrate())
|
||||
tv.UpdateTitle()
|
||||
}
|
||||
|
||||
func (v *benchView) keyBindings() {
|
||||
func (b *Bench) keyBindings() {
|
||||
aa := ui.KeyActions{
|
||||
ui.KeyP: ui.NewKeyAction("Previous", v.app.prevCmd, false),
|
||||
tcell.KeyEnter: ui.NewKeyAction("Enter", v.enterCmd, false),
|
||||
tcell.KeyCtrlD: ui.NewKeyAction("Delete", v.deleteCmd, false),
|
||||
ui.KeyP: ui.NewKeyAction("Previous", b.app.PrevCmd, false),
|
||||
tcell.KeyEnter: ui.NewKeyAction("Enter", b.enterCmd, false),
|
||||
tcell.KeyCtrlD: ui.NewKeyAction("Delete", b.deleteCmd, false),
|
||||
}
|
||||
v.masterPage().SetActions(aa)
|
||||
b.masterPage().AddActions(aa)
|
||||
}
|
||||
|
||||
func (v *benchView) getTitle() string {
|
||||
func (b *Bench) getTitle() string {
|
||||
return benchTitle
|
||||
}
|
||||
|
||||
func (v *benchView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
func (b *Bench) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
tv := v.masterPage()
|
||||
tv := b.masterPage()
|
||||
tv.SetSortCol(tv.NameColIndex()+col, 0, asc)
|
||||
tv.Refresh()
|
||||
|
||||
|
|
@ -107,69 +122,73 @@ func (v *benchView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tce
|
|||
}
|
||||
}
|
||||
|
||||
func (v *benchView) enterCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if v.masterPage().SearchBuff().IsActive() {
|
||||
return v.masterPage().filterCmd(evt)
|
||||
func (b *Bench) enterCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if b.masterPage().SearchBuff().IsActive() {
|
||||
return b.masterPage().filterCmd(evt)
|
||||
}
|
||||
|
||||
if !v.masterPage().RowSelected() {
|
||||
if !b.masterPage().RowSelected() {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := readBenchFile(v.app.Config, v.benchFile())
|
||||
data, err := readBenchFile(b.app.Config, b.benchFile())
|
||||
if err != nil {
|
||||
v.app.Flash().Errf("Unable to load bench file %s", err)
|
||||
b.app.Flash().Errf("Unable to load bench file %s", err)
|
||||
return nil
|
||||
}
|
||||
vu := v.detailsPage()
|
||||
vu := b.detailsPage()
|
||||
vu.SetText(data)
|
||||
vu.setTitle(v.masterPage().GetSelectedItem())
|
||||
v.showDetails()
|
||||
vu.setTitle(b.masterPage().GetSelectedItem())
|
||||
b.showDetails()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *benchView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !v.masterPage().RowSelected() {
|
||||
func (b *Bench) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !b.masterPage().RowSelected() {
|
||||
return nil
|
||||
}
|
||||
|
||||
sel, file := v.masterPage().GetSelectedItem(), v.benchFile()
|
||||
dir := filepath.Join(perf.K9sBenchDir, v.app.Config.K9s.CurrentCluster)
|
||||
showModal(v.Pages, fmt.Sprintf("Delete benchmark `%s?", file), "master", func() {
|
||||
sel, file := b.masterPage().GetSelectedItem(), b.benchFile()
|
||||
dir := filepath.Join(perf.K9sBenchDir, b.app.Config.K9s.CurrentCluster)
|
||||
showModal(b.Pages, fmt.Sprintf("Delete benchmark `%s?", file), "master", func() {
|
||||
if err := os.Remove(filepath.Join(dir, file)); err != nil {
|
||||
v.app.Flash().Errf("Unable to delete file %s", err)
|
||||
b.app.Flash().Errf("Unable to delete file %s", err)
|
||||
return
|
||||
}
|
||||
v.app.Flash().Infof("Benchmark %s deleted!", sel)
|
||||
b.app.Flash().Infof("Benchmark %s deleted!", sel)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *benchView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
v.showMaster()
|
||||
func (b *Bench) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
b.showMaster()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *benchView) benchFile() string {
|
||||
r := v.masterPage().GetSelectedRow()
|
||||
return ui.TrimCell(v.masterPage().Table, r, 7)
|
||||
func (b *Bench) benchFile() string {
|
||||
r := b.masterPage().GetSelectedRowIndex()
|
||||
return ui.TrimCell(b.masterPage().Table, r, 7)
|
||||
}
|
||||
|
||||
func (v *benchView) hints() ui.Hints {
|
||||
return v.CurrentPage().Item.(ui.Hinter).Hints()
|
||||
func (b *Bench) Hints() model.MenuHints {
|
||||
if h, ok := b.CurrentPage().Item.(model.Hinter); ok {
|
||||
return h.Hints()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *benchView) hydrate() resource.TableData {
|
||||
ff, err := loadBenchDir(v.app.Config)
|
||||
func (b *Bench) hydrate() resource.TableData {
|
||||
ff, err := loadBenchDir(b.app.Config)
|
||||
if err != nil {
|
||||
v.app.Flash().Errf("Unable to read bench directory %s", err)
|
||||
b.app.Flash().Errf("Unable to read bench directory %s", err)
|
||||
}
|
||||
|
||||
data := initTable()
|
||||
for _, f := range ff {
|
||||
bench, err := readBenchFile(v.app.Config, f.Name())
|
||||
bench, err := readBenchFile(b.app.Config, f.Name())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Unable to load bench file %s", f.Name())
|
||||
continue
|
||||
|
|
@ -203,7 +222,7 @@ func initRow(row resource.Row, f os.FileInfo) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (v *benchView) watchBenchDir(ctx context.Context) error {
|
||||
func (b *Bench) watchBenchDir(ctx context.Context) error {
|
||||
w, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -214,8 +233,8 @@ func (v *benchView) watchBenchDir(ctx context.Context) error {
|
|||
select {
|
||||
case evt := <-w.Events:
|
||||
log.Debug().Msgf("Bench event %#v", evt)
|
||||
v.app.QueueUpdateDraw(func() {
|
||||
v.refresh()
|
||||
b.app.QueueUpdateDraw(func() {
|
||||
b.refresh()
|
||||
})
|
||||
case err := <-w.Errors:
|
||||
log.Info().Err(err).Msg("Dir Watcher failed")
|
||||
|
|
@ -228,7 +247,7 @@ func (v *benchView) watchBenchDir(ctx context.Context) error {
|
|||
}
|
||||
}()
|
||||
|
||||
return w.Add(benchDir(v.app.Config))
|
||||
return w.Add(benchDir(b.app.Config))
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package views
|
||||
package view
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package views
|
||||
package view
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
|
@ -17,7 +17,7 @@ import (
|
|||
type clusterInfoView struct {
|
||||
*tview.Table
|
||||
|
||||
app *appView
|
||||
app *App
|
||||
mxs resource.MetricsServer
|
||||
}
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ type ClusterInfo interface {
|
|||
CurrentMEM() float64
|
||||
}
|
||||
|
||||
func newClusterInfoView(app *appView, mx resource.MetricsServer) *clusterInfoView {
|
||||
func newClusterInfoView(app *App, mx resource.MetricsServer) *clusterInfoView {
|
||||
return &clusterInfoView{
|
||||
app: app,
|
||||
Table: tview.NewTable(),
|
||||
|
|
@ -125,7 +125,7 @@ func (v *clusterInfoView) refresh() {
|
|||
v.refreshMetrics(cluster, row)
|
||||
}
|
||||
|
||||
func fetchResources(app *appView) (k8s.Collection, k8s.Collection, error) {
|
||||
func fetchResources(app *App) (k8s.Collection, k8s.Collection, error) {
|
||||
nos, err := app.informer.List(watch.NodeIndex, "", metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package views
|
||||
package view
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package views
|
||||
package view
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package views
|
||||
package view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
@ -6,46 +6,27 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type subjectViewer interface {
|
||||
resourceViewer
|
||||
type SubjectViewer interface {
|
||||
ResourceViewer
|
||||
|
||||
setSubject(s string)
|
||||
}
|
||||
|
||||
type command struct {
|
||||
app *appView
|
||||
history *ui.CmdStack
|
||||
app *App
|
||||
}
|
||||
|
||||
func newCommand(app *appView) *command {
|
||||
return &command{app: app, history: ui.NewCmdStack()}
|
||||
func newCommand(app *App) *command {
|
||||
return &command{app: app}
|
||||
}
|
||||
|
||||
func (c *command) lastCmd() bool {
|
||||
return c.history.Last()
|
||||
}
|
||||
|
||||
func (c *command) pushCmd(cmd string) {
|
||||
c.history.Push(cmd)
|
||||
c.app.Crumbs().Refresh(c.history.Items())
|
||||
}
|
||||
|
||||
func (c *command) previousCmd() (string, bool) {
|
||||
c.history.Pop()
|
||||
c.app.Crumbs().Refresh(c.history.Items())
|
||||
|
||||
return c.history.Top()
|
||||
}
|
||||
|
||||
// DefaultCmd reset default command ie show pods.
|
||||
func (c *command) defaultCmd() {
|
||||
cmd := c.app.Config.ActiveView()
|
||||
c.pushCmd(cmd)
|
||||
if !c.run(cmd) {
|
||||
log.Error().Err(fmt.Errorf("Unable to load command %s", cmd)).Msg("Command failed")
|
||||
}
|
||||
|
|
@ -71,7 +52,7 @@ func (c *command) isK9sCmd(cmd string) bool {
|
|||
}
|
||||
tokens := authRX.FindAllStringSubmatch(cmd, -1)
|
||||
if len(tokens) == 1 && len(tokens[0]) == 3 {
|
||||
c.app.inject(newPolicyView(c.app, tokens[0][1], tokens[0][2]))
|
||||
c.app.inject(NewPolicy(c.app, tokens[0][1], tokens[0][2]))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -122,7 +103,7 @@ func (c *command) run(cmd string) bool {
|
|||
c.app.switchCtx(cmds[1], true)
|
||||
return true
|
||||
}
|
||||
view := c.viewerFor(gvr, v)
|
||||
view := c.componentFor(gvr, v)
|
||||
return c.exec(gvr, "", view)
|
||||
default:
|
||||
ns := c.app.Config.ActiveNamespace()
|
||||
|
|
@ -132,23 +113,21 @@ func (c *command) run(cmd string) bool {
|
|||
if !c.app.switchNS(ns) {
|
||||
return false
|
||||
}
|
||||
return c.exec(gvr, ns, c.viewerFor(gvr, v))
|
||||
return c.exec(gvr, ns, c.componentFor(gvr, v))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *command) viewerFor(gvr string, v *viewer) resourceViewer {
|
||||
func (c *command) componentFor(gvr string, v *viewer) ResourceViewer {
|
||||
var r resource.List
|
||||
if v.listFn != nil {
|
||||
r = v.listFn(c.app.Conn(), resource.DefaultNamespace)
|
||||
}
|
||||
|
||||
var view resourceViewer
|
||||
var view ResourceViewer
|
||||
if v.viewFn != nil {
|
||||
view = v.viewFn(v.kind, gvr, c.app, r)
|
||||
view = v.viewFn(v.kind, gvr, r)
|
||||
} else {
|
||||
view = newResourceView(v.kind, gvr, c.app, r)
|
||||
view = NewResource(v.kind, gvr, r)
|
||||
}
|
||||
if v.colorerFn != nil {
|
||||
view.setColorerFn(v.colorerFn)
|
||||
|
|
@ -163,9 +142,9 @@ func (c *command) viewerFor(gvr string, v *viewer) resourceViewer {
|
|||
return view
|
||||
}
|
||||
|
||||
func (c *command) exec(gvr string, ns string, v ui.Igniter) bool {
|
||||
if v == nil {
|
||||
log.Error().Err(fmt.Errorf("No igniter given for %s", gvr))
|
||||
func (c *command) exec(gvr string, ns string, comp model.Component) bool {
|
||||
if comp == nil {
|
||||
log.Error().Err(fmt.Errorf("No component given for %s", gvr))
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -173,8 +152,10 @@ func (c *command) exec(gvr string, ns string, v ui.Igniter) bool {
|
|||
c.app.Flash().Infof("Viewing %s resource...", g.ToR())
|
||||
log.Debug().Msgf("Running command %s", gvr)
|
||||
c.app.Config.SetActiveView(g.ToR())
|
||||
c.app.Config.Save()
|
||||
c.app.inject(v)
|
||||
if err := c.app.Config.Save(); err != nil {
|
||||
log.Error().Err(err).Msg("Config save failed!")
|
||||
}
|
||||
c.app.inject(comp)
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package view
|
||||
|
||||
// import (
|
||||
// "testing"
|
||||
|
||||
// "github.com/derailed/k9s/internal/config"
|
||||
// "github.com/stretchr/testify/assert"
|
||||
// )
|
||||
|
||||
// func TestCommandPush(t *testing.T) {
|
||||
// c := newCommand(NewApp(config.NewConfig(ks{})))
|
||||
// c.pushCmd("fred")
|
||||
// c.pushCmd("blee")
|
||||
// p, top := c.previousCmd()
|
||||
|
||||
// assert.Equal(t, "fred", p)
|
||||
// assert.True(t, top)
|
||||
// assert.True(t, c.lastCmd())
|
||||
// }
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/k9s/internal/ui/dialog"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
"k8s.io/client-go/tools/portforward"
|
||||
)
|
||||
|
||||
// Container represents a container view.
|
||||
type Container struct {
|
||||
*LogResource
|
||||
}
|
||||
|
||||
// New Container returns a new container view.
|
||||
func NewContainer(title string, list resource.List, path string) ResourceViewer {
|
||||
c := Container{
|
||||
LogResource: NewLogResource(title, "", list),
|
||||
}
|
||||
c.path = &path
|
||||
c.envFn = c.k9sEnv
|
||||
c.containerFn = c.selectedContainer
|
||||
c.extraActionsFn = c.extraActions
|
||||
c.enterFn = c.viewLogs
|
||||
c.colorerFn = containerColorer
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// Init initializes a container view.
|
||||
func (c *Container) Init(ctx context.Context) {
|
||||
c.Resource.Init(ctx)
|
||||
}
|
||||
|
||||
// Start starts the component.
|
||||
func (c *Container) Start() {}
|
||||
|
||||
// Stop stops the component.
|
||||
func (c *Container) Stop() {}
|
||||
|
||||
// Name returns the component name.
|
||||
func (c *Container) Name() string { return "containers" }
|
||||
|
||||
func (c *Container) extraActions(aa ui.KeyActions) {
|
||||
c.LogResource.extraActions(aa)
|
||||
|
||||
aa[ui.KeyShiftF] = ui.NewKeyAction("PortForward", c.portFwdCmd, true)
|
||||
aa[ui.KeyShiftL] = ui.NewKeyAction("Logs Previous", c.prevLogsCmd, true)
|
||||
aa[ui.KeyS] = ui.NewKeyAction("Shell", c.shellCmd, true)
|
||||
aa[tcell.KeyEscape] = ui.NewKeyAction("Back", c.backCmd, false)
|
||||
aa[ui.KeyP] = ui.NewKeyAction("Previous", c.backCmd, false)
|
||||
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", c.sortColCmd(6, false), false)
|
||||
aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", c.sortColCmd(7, false), false)
|
||||
aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", c.sortColCmd(8, false), false)
|
||||
aa[ui.KeyShiftZ] = ui.NewKeyAction("Sort MEM%", c.sortColCmd(9, false), false)
|
||||
}
|
||||
|
||||
func (c *Container) k9sEnv() K9sEnv {
|
||||
env := c.defaultK9sEnv()
|
||||
|
||||
ns, n := namespaced(*c.path)
|
||||
env["POD"] = n
|
||||
env["NAMESPACE"] = ns
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
func (c *Container) selectedContainer() string {
|
||||
return c.masterPage().GetSelectedItem()
|
||||
}
|
||||
|
||||
func (c *Container) viewLogs(app *App, _, res, sel string) {
|
||||
status := c.masterPage().GetSelectedCell(3)
|
||||
if status == "Running" || status == "Completed" {
|
||||
c.showLogs(false)
|
||||
return
|
||||
}
|
||||
c.app.Flash().Err(errors.New("No logs available"))
|
||||
}
|
||||
|
||||
// Handlers...
|
||||
|
||||
func (c *Container) shellCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !c.masterPage().RowSelected() {
|
||||
return evt
|
||||
}
|
||||
|
||||
c.Stop()
|
||||
{
|
||||
shellIn(c.app, *c.path, c.masterPage().GetSelectedItem())
|
||||
}
|
||||
c.Start()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !c.masterPage().RowSelected() {
|
||||
return evt
|
||||
}
|
||||
|
||||
sel := c.masterPage().GetSelectedItem()
|
||||
if _, ok := c.app.forwarders[fwFQN(*c.path, sel)]; ok {
|
||||
c.app.Flash().Err(fmt.Errorf("A PortForward already exist on container %s", *c.path))
|
||||
return nil
|
||||
}
|
||||
|
||||
state := c.masterPage().GetSelectedCell(3)
|
||||
if state != "Running" {
|
||||
c.app.Flash().Err(fmt.Errorf("Container %s is not running?", sel))
|
||||
return nil
|
||||
}
|
||||
|
||||
portC := c.masterPage().GetSelectedCell(10)
|
||||
ports := strings.Split(portC, ",")
|
||||
if len(ports) == 0 {
|
||||
c.app.Flash().Err(errors.New("Container exposes no ports"))
|
||||
return nil
|
||||
}
|
||||
|
||||
var port string
|
||||
for _, p := range ports {
|
||||
log.Debug().Msgf("Checking port %q", p)
|
||||
if !isTCPPort(p) {
|
||||
continue
|
||||
}
|
||||
port = strings.TrimSpace(p)
|
||||
break
|
||||
}
|
||||
if port == "" {
|
||||
c.app.Flash().Warn("No valid TCP port found on this container. User will specify...")
|
||||
port = "MY_TCP_PORT!"
|
||||
}
|
||||
dialog.ShowPortForward(c.Pages, port, c.portForward)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Container) portForward(lport, cport string) {
|
||||
co := c.masterPage().GetSelectedCell(0)
|
||||
pf := k8s.NewPortForward(c.app.Conn(), &log.Logger)
|
||||
ports := []string{lport + ":" + cport}
|
||||
fw, err := pf.Start(*c.path, co, ports)
|
||||
if err != nil {
|
||||
c.app.Flash().Err(err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Msgf(">>> Starting port forward %q %v", *c.path, ports)
|
||||
go c.runForward(pf, fw)
|
||||
}
|
||||
|
||||
func (c *Container) runForward(pf *k8s.PortForward, f *portforward.PortForwarder) {
|
||||
c.app.QueueUpdateDraw(func() {
|
||||
c.app.forwarders[pf.FQN()] = pf
|
||||
c.app.Flash().Infof("PortForward activated %s:%s", pf.Path(), pf.Ports()[0])
|
||||
dialog.DismissPortForward(c.Pages)
|
||||
})
|
||||
|
||||
pf.SetActive(true)
|
||||
if err := f.ForwardPorts(); err != nil {
|
||||
c.app.Flash().Err(err)
|
||||
return
|
||||
}
|
||||
c.app.QueueUpdateDraw(func() {
|
||||
delete(c.app.forwarders, pf.FQN())
|
||||
pf.SetActive(false)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Container) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return c.app.PrevCmd(evt)
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
)
|
||||
|
||||
// Context presents a context viewer.
|
||||
type Context struct {
|
||||
*Resource
|
||||
}
|
||||
|
||||
// NewContext return a new context viewer.
|
||||
func NewContext(title, gvr string, list resource.List) ResourceViewer {
|
||||
c := Context{
|
||||
Resource: NewResource(title, gvr, list),
|
||||
}
|
||||
c.extraActionsFn = c.extraActions
|
||||
c.enterFn = c.useCtx
|
||||
c.masterPage().SetSelectedFn(c.cleanser)
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
func (c *Context) extraActions(aa ui.KeyActions) {
|
||||
c.masterPage().RmAction(ui.KeyShiftA)
|
||||
}
|
||||
|
||||
func (c *Context) useCtx(app *App, _, res, sel string) {
|
||||
if err := c.useContext(sel); err != nil {
|
||||
app.Flash().Err(err)
|
||||
return
|
||||
}
|
||||
app.gotoResource("po", true)
|
||||
}
|
||||
|
||||
func (*Context) cleanser(s string) string {
|
||||
name := strings.TrimSpace(s)
|
||||
if strings.HasSuffix(name, "*") {
|
||||
name = strings.TrimRight(name, "*")
|
||||
}
|
||||
if strings.HasSuffix(name, "(𝜟)") {
|
||||
name = strings.TrimRight(name, "(𝜟)")
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func (c *Context) useContext(name string) error {
|
||||
ctx := c.cleanser(name)
|
||||
if err := c.list.Resource().(*resource.Context).Switch(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.app.switchCtx(name, false)
|
||||
c.refresh()
|
||||
if tv, ok := c.GetPrimitive("ctx").(*Table); ok {
|
||||
tv.Select(1, 0)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package view_test
|
||||
|
||||
// import (
|
||||
// "testing"
|
||||
|
||||
// "github.com/derailed/k9s/internal/config"
|
||||
// "github.com/derailed/k9s/internal/resource"
|
||||
// "github.com/derailed/k9s/internal/view"
|
||||
// "github.com/stretchr/testify/assert"
|
||||
// )
|
||||
|
||||
// func TestContext(t *testing.T) {
|
||||
// l := resource.NewContextList(nil, "fred")
|
||||
// v := view.NewContext("blee", "", NewApp(config.NewConfig(ks{})), l).(*contextView)
|
||||
|
||||
// assert.Equal(t, 10, len(v.Hints()))
|
||||
// }
|
||||
|
||||
// func TestCleaner(t *testing.T) {
|
||||
// uu := map[string]struct {
|
||||
// s, e string
|
||||
// }{
|
||||
// "normal": {"fred", "fred"},
|
||||
// "default": {"fred*", "fred"},
|
||||
// "delta": {"fred(𝜟)", "fred"},
|
||||
// }
|
||||
|
||||
// v := contextView{}
|
||||
// for k, u := range uu {
|
||||
// t.Run(k, func(t *testing.T) {
|
||||
// assert.Equal(t, u.e, v.cleanser(u.s))
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
// CronJob presents a cronjob viewer.
|
||||
type CronJob struct {
|
||||
*Resource
|
||||
}
|
||||
|
||||
// NewCronJob returns a new viewer.
|
||||
func NewCronJob(title, gvr string, list resource.List) ResourceViewer {
|
||||
c := CronJob{
|
||||
Resource: NewResource(title, gvr, list),
|
||||
}
|
||||
c.extraActionsFn = c.extraActions
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
func (c *CronJob) trigger(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !c.masterPage().RowSelected() {
|
||||
return evt
|
||||
}
|
||||
|
||||
sel := c.masterPage().GetSelectedItem()
|
||||
if err := c.list.Resource().(resource.Runner).Run(sel); err != nil {
|
||||
c.app.Flash().Errf("Cronjob trigger failed %v", err)
|
||||
return evt
|
||||
}
|
||||
c.app.Flash().Infof("Triggering %s %s", c.list.GetName(), sel)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CronJob) extraActions(aa ui.KeyActions) {
|
||||
aa[tcell.KeyCtrlT] = ui.NewKeyAction("Trigger", c.trigger, true)
|
||||
}
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const detailsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] "
|
||||
|
||||
// Details presents a generic text viewer.
|
||||
type Details struct {
|
||||
*tview.TextView
|
||||
|
||||
app *App
|
||||
actions ui.KeyActions
|
||||
cmdBuff *ui.CmdBuff
|
||||
title string
|
||||
category string
|
||||
backFn ui.ActionHandler
|
||||
numSelections int
|
||||
}
|
||||
|
||||
// NewDetails returns a details viewer.
|
||||
func NewDetails(app *App, backFn ui.ActionHandler) *Details {
|
||||
return &Details{
|
||||
TextView: tview.NewTextView(),
|
||||
app: app,
|
||||
backFn: backFn,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Details) Init(ctx context.Context) {
|
||||
d.app = ctx.Value(ui.KeyApp).(*App)
|
||||
|
||||
d.SetScrollable(true)
|
||||
d.SetWrap(true)
|
||||
d.SetDynamicColors(true)
|
||||
d.SetRegions(true)
|
||||
d.SetBorder(true)
|
||||
d.SetBorderFocusColor(config.AsColor(d.app.Styles.Frame().Border.FocusColor))
|
||||
d.SetHighlightColor(tcell.ColorOrange)
|
||||
d.SetTitleColor(tcell.ColorAqua)
|
||||
d.SetInputCapture(d.keyboard)
|
||||
d.bindKeys()
|
||||
|
||||
d.cmdBuff = ui.NewCmdBuff('/', ui.FilterBuff)
|
||||
d.cmdBuff.AddListener(d.app.Cmd())
|
||||
d.cmdBuff.Reset()
|
||||
|
||||
d.SetChangedFunc(func() {
|
||||
d.app.Draw()
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Details) Name() string { return "details" }
|
||||
func (d *Details) Start() {}
|
||||
func (d *Details) Stop() {}
|
||||
|
||||
func (d *Details) bindKeys() {
|
||||
d.actions = ui.KeyActions{
|
||||
tcell.KeyBackspace2: ui.NewKeyAction("Erase", d.eraseCmd, false),
|
||||
tcell.KeyBackspace: ui.NewKeyAction("Erase", d.eraseCmd, false),
|
||||
tcell.KeyDelete: ui.NewKeyAction("Erase", d.eraseCmd, false),
|
||||
tcell.KeyEscape: ui.NewKeyAction("Back", d.backCmd, true),
|
||||
tcell.KeyTab: ui.NewKeyAction("Next Match", d.nextCmd, false),
|
||||
tcell.KeyBacktab: ui.NewKeyAction("Previous Match", d.prevCmd, false),
|
||||
tcell.KeyCtrlS: ui.NewKeyAction("Save", d.saveCmd, true),
|
||||
ui.KeyC: ui.NewKeyAction("Copy", d.cpCmd, false),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Details) setCategory(n string) {
|
||||
d.category = n
|
||||
}
|
||||
|
||||
func (d *Details) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
key := evt.Key()
|
||||
if key == tcell.KeyRune {
|
||||
if d.cmdBuff.IsActive() {
|
||||
d.cmdBuff.Add(evt.Rune())
|
||||
d.refreshTitle()
|
||||
return nil
|
||||
}
|
||||
key = tcell.Key(evt.Rune())
|
||||
}
|
||||
|
||||
if a, ok := d.actions[key]; ok {
|
||||
log.Debug().Msgf(">> DetailsView handled %s", tcell.KeyNames[key])
|
||||
return a.Action(evt)
|
||||
}
|
||||
return evt
|
||||
}
|
||||
|
||||
func (d *Details) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if path, err := saveYAML(d.app.Config.K9s.CurrentCluster, d.title, d.GetText(true)); err != nil {
|
||||
d.app.Flash().Err(err)
|
||||
} else {
|
||||
d.app.Flash().Infof("Log %s saved successfully!", path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Details) cpCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
d.app.Flash().Info("Content copied to clipboard...")
|
||||
if err := clipboard.WriteAll(d.GetText(true)); err != nil {
|
||||
d.app.Flash().Err(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Details) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !d.cmdBuff.Empty() {
|
||||
d.cmdBuff.Reset()
|
||||
d.search(evt)
|
||||
return nil
|
||||
}
|
||||
d.cmdBuff.Reset()
|
||||
if d.backFn != nil {
|
||||
return d.backFn(evt)
|
||||
}
|
||||
return evt
|
||||
}
|
||||
|
||||
func (d *Details) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !d.cmdBuff.IsActive() {
|
||||
return evt
|
||||
}
|
||||
d.cmdBuff.Delete()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Details) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !d.app.InCmdMode() {
|
||||
d.cmdBuff.SetActive(true)
|
||||
d.cmdBuff.Clear()
|
||||
return nil
|
||||
}
|
||||
return evt
|
||||
}
|
||||
|
||||
func (d *Details) searchCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if d.cmdBuff.IsActive() && !d.cmdBuff.Empty() {
|
||||
d.app.Flash().Infof("Searching for %s...", d.cmdBuff)
|
||||
d.search(evt)
|
||||
highlights := d.GetHighlights()
|
||||
if len(highlights) > 0 {
|
||||
d.Highlight()
|
||||
} else {
|
||||
d.Highlight("0").ScrollToHighlight()
|
||||
}
|
||||
}
|
||||
d.cmdBuff.SetActive(false)
|
||||
return evt
|
||||
}
|
||||
|
||||
func (d *Details) search(evt *tcell.EventKey) {
|
||||
d.numSelections = 0
|
||||
log.Debug().Msgf("Searching... %s - %d", d.cmdBuff, d.numSelections)
|
||||
d.Highlight("")
|
||||
d.SetText(d.decorateLines(d.GetText(false), d.cmdBuff.String()))
|
||||
|
||||
if d.cmdBuff.Empty() {
|
||||
d.app.Flash().Info("Clearing out search query...")
|
||||
d.refreshTitle()
|
||||
return
|
||||
}
|
||||
if d.numSelections == 0 {
|
||||
d.app.Flash().Warn("No matches found!")
|
||||
return
|
||||
}
|
||||
d.app.Flash().Infof("Found <%d> matches! <tab>/<TAB> for next/previous", d.numSelections)
|
||||
}
|
||||
|
||||
func (d *Details) nextCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
highlights := d.GetHighlights()
|
||||
if len(highlights) == 0 || d.numSelections == 0 {
|
||||
return evt
|
||||
}
|
||||
index, _ := strconv.Atoi(highlights[0])
|
||||
index = (index + 1) % d.numSelections
|
||||
if index+1 == d.numSelections {
|
||||
d.app.Flash().Info("Search hit BOTTOM, continuing at TOP")
|
||||
}
|
||||
d.Highlight(strconv.Itoa(index)).ScrollToHighlight()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Details) prevCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
highlights := d.GetHighlights()
|
||||
if len(highlights) == 0 || d.numSelections == 0 {
|
||||
return evt
|
||||
}
|
||||
index, _ := strconv.Atoi(highlights[0])
|
||||
index = (index - 1 + d.numSelections) % d.numSelections
|
||||
if index == 0 {
|
||||
d.app.Flash().Info("Search hit TOP, continuing at BOTTOM")
|
||||
}
|
||||
d.Highlight(strconv.Itoa(index)).ScrollToHighlight()
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetActions to handle keyboard inputs
|
||||
func (d *Details) setActions(aa ui.KeyActions) {
|
||||
for k, a := range aa {
|
||||
d.actions[k] = a
|
||||
}
|
||||
}
|
||||
|
||||
// Hints fetch mmemonic and hints
|
||||
func (d *Details) Hints() model.MenuHints {
|
||||
if d.actions != nil {
|
||||
return d.actions.Hints()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Details) refreshTitle() {
|
||||
d.setTitle(d.title)
|
||||
}
|
||||
|
||||
func (d *Details) setTitle(t string) {
|
||||
d.title = t
|
||||
|
||||
title := skinTitle(fmt.Sprintf(detailsTitleFmt, d.category, t), d.app.Styles.Frame())
|
||||
if !d.cmdBuff.Empty() {
|
||||
title += skinTitle(fmt.Sprintf(ui.SearchFmt, d.cmdBuff.String()), d.app.Styles.Frame())
|
||||
}
|
||||
d.SetTitle(title)
|
||||
}
|
||||
|
||||
var (
|
||||
regionRX = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`)
|
||||
escapeRX = regexp.MustCompile(`\[([a-zA-Z0-9_,;: \-\."#]+)\[(\[*)\]`)
|
||||
)
|
||||
|
||||
func (d *Details) decorateLines(buff, q string) string {
|
||||
rx := regexp.MustCompile(`(?i)` + q)
|
||||
lines := strings.Split(buff, "\n")
|
||||
for i, l := range lines {
|
||||
l = regionRX.ReplaceAllString(l, "")
|
||||
l = escapeRX.ReplaceAllString(l, "")
|
||||
if m := rx.FindString(l); len(m) > 0 {
|
||||
lines[i] = rx.ReplaceAllString(l, fmt.Sprintf(`["%d"]%s[""]`, d.numSelections, m))
|
||||
d.numSelections++
|
||||
continue
|
||||
}
|
||||
lines[i] = l
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package view_test
|
||||
|
||||
// import (
|
||||
// "testing"
|
||||
|
||||
// "github.com/derailed/k9s/internal/config"
|
||||
// "github.com/derailed/k9s/internal/view"
|
||||
// "github.com/stretchr/testify/assert"
|
||||
// )
|
||||
|
||||
// func TestDetailsDecorateLines(t *testing.T) {
|
||||
// buff := `
|
||||
// I love blee
|
||||
// blee is much [blue::]cooler [green::]than foo!
|
||||
// `
|
||||
// exp := `
|
||||
// I love ["0"]blee[""]
|
||||
// ["1"]blee[""] is much [blue::]cooler [green::]than foo!
|
||||
// `
|
||||
|
||||
// app := view.NewApp(config.NewConfig(ks{}))
|
||||
// v := view.NewDetails{app: app}
|
||||
|
||||
// assert.Equal(t, exp, v.decorateLines(buff, "blee"))
|
||||
// }
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
v1 "k8s.io/api/apps/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const scaleDialogKey = "scale"
|
||||
|
||||
// Deploy represents a deployment view.
|
||||
type Deploy struct {
|
||||
*LogResource
|
||||
|
||||
scalableResource *ScalableResource
|
||||
restartableResource *RestartableResource
|
||||
}
|
||||
|
||||
// NewDeploy returns a new deployment view.
|
||||
func NewDeploy(title, gvr string, list resource.List) ResourceViewer {
|
||||
l := NewLogResource(title, gvr, list)
|
||||
d := Deploy{
|
||||
LogResource: l,
|
||||
scalableResource: newScalableResourceForParent(l.Resource),
|
||||
restartableResource: newRestartableResourceForParent(l.Resource),
|
||||
}
|
||||
d.extraActionsFn = d.extraActions
|
||||
d.enterFn = d.showPods
|
||||
|
||||
return &d
|
||||
}
|
||||
|
||||
func (d *Deploy) extraActions(aa ui.KeyActions) {
|
||||
d.LogResource.extraActions(aa)
|
||||
d.scalableResource.extraActions(aa)
|
||||
d.restartableResource.extraActions(aa)
|
||||
aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", d.sortColCmd(1, false), false)
|
||||
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", d.sortColCmd(2, false), false)
|
||||
}
|
||||
|
||||
func (d *Deploy) showPods(app *App, _, res, sel string) {
|
||||
ns, n := namespaced(sel)
|
||||
dep, err := k8s.NewDeployment(app.Conn()).Get(ns, n)
|
||||
if err != nil {
|
||||
app.Flash().Err(err)
|
||||
return
|
||||
}
|
||||
|
||||
dp := dep.(*v1.Deployment)
|
||||
l, err := metav1.LabelSelectorAsSelector(dp.Spec.Selector)
|
||||
if err != nil {
|
||||
app.Flash().Err(err)
|
||||
return
|
||||
}
|
||||
|
||||
showPods(app, ns, l.String(), "", d.backCmd)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package view_test
|
||||
|
||||
// import (
|
||||
// "testing"
|
||||
|
||||
// "github.com/derailed/k9s/internal/config"
|
||||
// "github.com/derailed/k9s/internal/resource"
|
||||
// "github.com/stretchr/testify/assert"
|
||||
// )
|
||||
|
||||
// func TestDeploy(t *testing.T) {
|
||||
// l := resource.NewDeploymentList(nil, "fred")
|
||||
// v := view.NewDeploy("blee", "", l)
|
||||
// ctx := context.WithValue(ui.KeyApp, NewApp(config.NewConfig(ks{})))
|
||||
// v.Init(ctx)
|
||||
|
||||
// assert.Equal(t, 10, len(v.Hints()))
|
||||
// }
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type DaemonSet struct {
|
||||
*LogResource
|
||||
|
||||
restartableResource *RestartableResource
|
||||
}
|
||||
|
||||
func NewDaemonSet(title, gvr string, list resource.List) ResourceViewer {
|
||||
l := NewLogResource(title, gvr, list)
|
||||
d := DaemonSet{
|
||||
LogResource: l,
|
||||
restartableResource: newRestartableResourceForParent(l.Resource),
|
||||
}
|
||||
d.extraActionsFn = d.extraActions
|
||||
d.enterFn = d.showPods
|
||||
|
||||
return &d
|
||||
}
|
||||
|
||||
func (d *DaemonSet) extraActions(aa ui.KeyActions) {
|
||||
d.LogResource.extraActions(aa)
|
||||
d.restartableResource.extraActions(aa)
|
||||
aa[ui.KeyShiftD] = ui.NewKeyAction("Sort Desired", d.sortColCmd(1, false), false)
|
||||
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", d.sortColCmd(2, false), false)
|
||||
}
|
||||
|
||||
func (d *DaemonSet) showPods(app *App, _, res, sel string) {
|
||||
ns, n := namespaced(sel)
|
||||
dset, err := k8s.NewDaemonSet(app.Conn()).Get(ns, n)
|
||||
if err != nil {
|
||||
d.app.Flash().Err(err)
|
||||
return
|
||||
}
|
||||
|
||||
ds := dset.(*appsv1.DaemonSet)
|
||||
l, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector)
|
||||
if err != nil {
|
||||
app.Flash().Err(err)
|
||||
return
|
||||
}
|
||||
|
||||
showPods(app, ns, l.String(), "", d.backCmd)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package view_test
|
||||
|
||||
// import (
|
||||
// "context"
|
||||
// "testing"
|
||||
|
||||
// "github.com/derailed/k9s/internal/config"
|
||||
// "github.com/derailed/k9s/internal/resource"
|
||||
// "github.com/derailed/k9s/internal/ui"
|
||||
// "github.com/derailed/k9s/internal/view"
|
||||
// "github.com/stretchr/testify/assert"
|
||||
// )
|
||||
|
||||
// func TestDaemonSet(t *testing.T) {
|
||||
// l := resource.NewDaemonSetList(nil, "fred")
|
||||
// v := view.NewDaemonSet("blee", "", l)
|
||||
// ctx := context.WithValue(ui.KeyApp, NewApp(config.NewConfig(ks{})))
|
||||
// v.Init(ctx)
|
||||
|
||||
// assert.Equal(t, 10, len(v.Hints()))
|
||||
// }
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
dumpTitle = "Screen Dumps"
|
||||
dumpTitleFmt = " [mediumvioletred::b]%s([fuchsia::b]%d[fuchsia::-])[mediumvioletred::-] "
|
||||
)
|
||||
|
||||
var (
|
||||
dumpHeader = resource.Row{"NAME", "AGE"}
|
||||
)
|
||||
|
||||
// ScreenDump presents a directory listing viewer.
|
||||
type ScreenDump struct {
|
||||
*MasterDetail
|
||||
|
||||
cancelFn context.CancelFunc
|
||||
app *App
|
||||
}
|
||||
|
||||
func NewScreenDump(_, _ string, _ resource.List) ResourceViewer {
|
||||
return &ScreenDump{
|
||||
MasterDetail: NewMasterDetail(),
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes the viewer.
|
||||
func (s *ScreenDump) Init(ctx context.Context) {
|
||||
s.app = ctx.Value(ui.KeyApp).(*App)
|
||||
|
||||
table := s.masterPage()
|
||||
table.SetBorderFocusColor(tcell.ColorSteelBlue)
|
||||
table.SetSelectedStyle(tcell.ColorWhite, tcell.ColorRoyalBlue, tcell.AttrNone)
|
||||
table.SetColorerFn(dumpColorer)
|
||||
table.SetActiveNS(resource.AllNamespaces)
|
||||
table.SetSortCol(table.NameColIndex()+1, 0, true)
|
||||
table.SelectRow(1, true)
|
||||
|
||||
s.Start()
|
||||
s.refresh()
|
||||
}
|
||||
|
||||
// Start starts the directory watcher.
|
||||
func (s *ScreenDump) Start() {
|
||||
var ctx context.Context
|
||||
ctx, s.cancelFn = context.WithCancel(context.Background())
|
||||
if err := s.watchDumpDir(ctx); err != nil {
|
||||
s.app.Flash().Errf("Unable to watch dumpmarks directory %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop terminates the directory watcher.
|
||||
func (s *ScreenDump) Stop() {
|
||||
if s.cancelFn != nil {
|
||||
s.cancelFn()
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the component name.
|
||||
func (s *ScreenDump) Name() string {
|
||||
return dumpTitle
|
||||
}
|
||||
|
||||
func (s *ScreenDump) setEnterFn(enterFn) {}
|
||||
func (s *ScreenDump) setColorerFn(ui.ColorerFunc) {}
|
||||
func (s *ScreenDump) setDecorateFn(decorateFn) {}
|
||||
func (s *ScreenDump) setExtraActionsFn(ActionsFunc) {}
|
||||
|
||||
func (s *ScreenDump) refresh() {
|
||||
tv := s.masterPage()
|
||||
tv.Update(s.hydrate())
|
||||
tv.UpdateTitle()
|
||||
}
|
||||
|
||||
func (s *ScreenDump) registerActions() {
|
||||
aa := ui.KeyActions{
|
||||
ui.KeyP: ui.NewKeyAction("Previous", s.app.PrevCmd, false),
|
||||
tcell.KeyEnter: ui.NewKeyAction("Enter", s.enterCmd, true),
|
||||
tcell.KeyCtrlD: ui.NewKeyAction("Delete", s.deleteCmd, true),
|
||||
tcell.KeyCtrlS: ui.NewKeyAction("Save", noopCmd, false),
|
||||
}
|
||||
s.masterPage().AddActions(aa)
|
||||
}
|
||||
|
||||
func (s *ScreenDump) getTitle() string {
|
||||
return dumpTitle
|
||||
}
|
||||
|
||||
func (s *ScreenDump) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
tv := s.masterPage()
|
||||
tv.SetSortCol(tv.NameColIndex()+col, 0, asc)
|
||||
tv.Refresh()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ScreenDump) enterCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
log.Debug().Msg("Dump enter!")
|
||||
tv := s.masterPage()
|
||||
if tv.SearchBuff().IsActive() {
|
||||
return tv.filterCmd(evt)
|
||||
}
|
||||
sel := tv.GetSelectedItem()
|
||||
if sel == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
dir := filepath.Join(config.K9sDumpDir, s.app.Config.K9s.CurrentCluster)
|
||||
if !edit(true, s.app, filepath.Join(dir, sel)) {
|
||||
s.app.Flash().Err(errors.New("Failed to launch editor"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ScreenDump) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
sel := s.masterPage().GetSelectedItem()
|
||||
if sel == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
dir := filepath.Join(config.K9sDumpDir, s.app.Config.K9s.CurrentCluster)
|
||||
showModal(s.Pages, fmt.Sprintf("Delete screen dump `%s?", sel), "table", func() {
|
||||
if err := os.Remove(filepath.Join(dir, sel)); err != nil {
|
||||
s.app.Flash().Errf("Unable to delete file %s", err)
|
||||
return
|
||||
}
|
||||
s.refresh()
|
||||
s.app.Flash().Infof("ScreenDump file %s deleted!", sel)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ScreenDump) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if s.cancelFn != nil {
|
||||
s.cancelFn()
|
||||
}
|
||||
s.SwitchToPage("table")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ScreenDump) Hints() model.MenuHints {
|
||||
return s.Hints()
|
||||
}
|
||||
|
||||
func (s *ScreenDump) hydrate() resource.TableData {
|
||||
data := resource.TableData{
|
||||
Header: dumpHeader,
|
||||
Rows: make(resource.RowEvents, 10),
|
||||
Namespace: resource.NotNamespaced,
|
||||
}
|
||||
|
||||
dir := filepath.Join(config.K9sDumpDir, s.app.Config.K9s.CurrentCluster)
|
||||
ff, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
s.app.Flash().Errf("Unable to read dump directory %s", err)
|
||||
}
|
||||
|
||||
for _, f := range ff {
|
||||
fields := resource.Row{f.Name(), time.Since(f.ModTime()).String()}
|
||||
data.Rows[f.Name()] = &resource.RowEvent{
|
||||
Action: resource.New,
|
||||
Fields: fields,
|
||||
Deltas: fields,
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func (s *ScreenDump) resetTitle() {
|
||||
s.SetTitle(fmt.Sprintf(dumpTitleFmt, dumpTitle, s.masterPage().GetRowCount()-1))
|
||||
}
|
||||
|
||||
func (s *ScreenDump) watchDumpDir(ctx context.Context) error {
|
||||
w, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case evt := <-w.Events:
|
||||
log.Debug().Msgf("Dump event %#v", evt)
|
||||
s.app.QueueUpdateDraw(func() {
|
||||
s.refresh()
|
||||
})
|
||||
case err := <-w.Errors:
|
||||
log.Info().Err(err).Msg("Dir Watcher failed")
|
||||
return
|
||||
case <-ctx.Done():
|
||||
log.Debug().Msg("!!!! FS WATCHER DONE!!")
|
||||
w.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return w.Add(filepath.Join(config.K9sDumpDir, s.app.Config.K9s.CurrentCluster))
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
||||
func noopCmd(*tcell.EventKey) *tcell.EventKey {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package views
|
||||
package view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package views
|
||||
package view
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package views
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -13,7 +13,7 @@ import (
|
|||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func runK(clear bool, app *appView, args ...string) bool {
|
||||
func runK(clear bool, app *App, args ...string) bool {
|
||||
bin, err := exec.LookPath("kubectl")
|
||||
if err != nil {
|
||||
log.Error().Msgf("Unable to find kubectl command in path %v", err)
|
||||
|
|
@ -23,7 +23,7 @@ func runK(clear bool, app *appView, args ...string) bool {
|
|||
return run(clear, app, bin, false, args...)
|
||||
}
|
||||
|
||||
func run(clear bool, app *appView, bin string, bg bool, args ...string) bool {
|
||||
func run(clear bool, app *App, bin string, bg bool, args ...string) bool {
|
||||
return app.Suspend(func() {
|
||||
if err := execute(clear, bin, bg, args...); err != nil {
|
||||
app.Flash().Errf("Command exited: %v", err)
|
||||
|
|
@ -31,7 +31,7 @@ func run(clear bool, app *appView, bin string, bg bool, args ...string) bool {
|
|||
})
|
||||
}
|
||||
|
||||
func edit(clear bool, app *appView, args ...string) bool {
|
||||
func edit(clear bool, app *App, args ...string) bool {
|
||||
bin, err := exec.LookPath(os.Getenv("EDITOR"))
|
||||
if err != nil {
|
||||
log.Error().Msgf("Unable to find editor command in path %v", err)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package views
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
|
|
@ -19,44 +20,51 @@ const (
|
|||
helpTitleFmt = " [aqua::b]%s "
|
||||
)
|
||||
|
||||
type (
|
||||
helpItem struct {
|
||||
key, description string
|
||||
}
|
||||
type helpItem struct {
|
||||
key, description string
|
||||
}
|
||||
|
||||
helpView struct {
|
||||
*tview.Table
|
||||
// Help presents a help viewer.
|
||||
type Help struct {
|
||||
*ui.Table
|
||||
|
||||
app *appView
|
||||
current ui.Igniter
|
||||
actions ui.KeyActions
|
||||
}
|
||||
)
|
||||
app *App
|
||||
actions ui.KeyActions
|
||||
}
|
||||
|
||||
func newHelpView(app *appView, current ui.Igniter, hh ui.Hints) *helpView {
|
||||
v := helpView{
|
||||
Table: tview.NewTable(),
|
||||
app: app,
|
||||
// NewHelp returns a new help viewer.
|
||||
func NewHelp() *Help {
|
||||
return &Help{
|
||||
Table: ui.NewTable(helpTitle),
|
||||
actions: make(ui.KeyActions),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Help) Init(ctx context.Context) {
|
||||
v.app = ctx.Value(ui.KeyApp).(*App)
|
||||
|
||||
v.resetTitle()
|
||||
|
||||
v.SetBorder(true)
|
||||
v.SetBorderPadding(0, 0, 1, 1)
|
||||
v.SetInputCapture(v.keyboard)
|
||||
v.current = current
|
||||
v.bindKeys()
|
||||
v.build(hh)
|
||||
|
||||
return &v
|
||||
v.build(v.app.Hint.Peek())
|
||||
}
|
||||
|
||||
func (v *helpView) bindKeys() {
|
||||
func (v *Help) Name() string { return helpTitle }
|
||||
func (v *Help) Start() {}
|
||||
func (v *Help) Stop() {}
|
||||
func (v *Help) Hints() model.MenuHints { return v.actions.Hints() }
|
||||
|
||||
func (v *Help) bindKeys() {
|
||||
v.actions = ui.KeyActions{
|
||||
tcell.KeyEsc: ui.NewKeyAction("Back", v.backCmd, true),
|
||||
tcell.KeyEnter: ui.NewKeyAction("Back", v.backCmd, false),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *helpView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
func (v *Help) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
key := evt.Key()
|
||||
if key == tcell.KeyRune {
|
||||
key = tcell.Key(evt.Rune())
|
||||
|
|
@ -66,21 +74,16 @@ func (v *helpView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
|||
log.Debug().Msgf(">> TableView handled %s", tcell.KeyNames[key])
|
||||
return a.Action(evt)
|
||||
}
|
||||
|
||||
return evt
|
||||
}
|
||||
|
||||
func (v *helpView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
v.app.inject(v.current)
|
||||
return nil
|
||||
func (v *Help) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return v.app.PrevCmd(evt)
|
||||
}
|
||||
|
||||
func (v *helpView) Init(_ context.Context, _ string) {
|
||||
v.resetTitle()
|
||||
v.app.SetHints(v.Hints())
|
||||
}
|
||||
|
||||
func (v *helpView) showHelp() ui.Hints {
|
||||
return ui.Hints{
|
||||
func (v *Help) showHelp() model.MenuHints {
|
||||
return model.MenuHints{
|
||||
{
|
||||
Mnemonic: "?",
|
||||
Description: "Help",
|
||||
|
|
@ -92,8 +95,8 @@ func (v *helpView) showHelp() ui.Hints {
|
|||
}
|
||||
}
|
||||
|
||||
func (v *helpView) showNav() ui.Hints {
|
||||
return ui.Hints{
|
||||
func (v *Help) showNav() model.MenuHints {
|
||||
return model.MenuHints{
|
||||
{
|
||||
Mnemonic: "g",
|
||||
Description: "Goto Top",
|
||||
|
|
@ -128,8 +131,8 @@ func (v *helpView) showNav() ui.Hints {
|
|||
}
|
||||
}
|
||||
|
||||
func (v *helpView) showGeneral() ui.Hints {
|
||||
return ui.Hints{
|
||||
func (v *Help) showGeneral() model.MenuHints {
|
||||
return model.MenuHints{
|
||||
{
|
||||
Mnemonic: ":cmd",
|
||||
Description: "Command mode",
|
||||
|
|
@ -173,19 +176,15 @@ func (v *helpView) showGeneral() ui.Hints {
|
|||
}
|
||||
}
|
||||
|
||||
func (v *helpView) Hints() ui.Hints {
|
||||
return v.actions.Hints()
|
||||
}
|
||||
|
||||
func (v *helpView) getTitle() string {
|
||||
func (v *Help) getTitle() string {
|
||||
return helpTitle
|
||||
}
|
||||
|
||||
func (v *helpView) resetTitle() {
|
||||
func (v *Help) resetTitle() {
|
||||
v.SetTitle(fmt.Sprintf(helpTitleFmt, helpTitle))
|
||||
}
|
||||
|
||||
func (v *helpView) build(hh ui.Hints) {
|
||||
func (v *Help) build(hh model.MenuHints) {
|
||||
v.Clear()
|
||||
sort.Sort(hh)
|
||||
v.addSection(0, 0, "RESOURCE", hh)
|
||||
|
|
@ -194,7 +193,7 @@ func (v *helpView) build(hh ui.Hints) {
|
|||
v.addSection(0, 8, "HELP", v.showHelp())
|
||||
}
|
||||
|
||||
func (v *helpView) addSection(r, c int, title string, hh ui.Hints) {
|
||||
func (v *Help) addSection(r, c int, title string, hh model.MenuHints) {
|
||||
row := r
|
||||
cell := tview.NewTableCell(title)
|
||||
cell.SetTextColor(tcell.ColorGreen)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package view_test
|
||||
|
||||
// import (
|
||||
// "testing"
|
||||
|
||||
// "github.com/derailed/k9s/internal/config"
|
||||
// "github.com/derailed/k9s/internal/model"
|
||||
// "github.com/stretchr/testify/assert"
|
||||
// v1 "k8s.io/api/core/v1"
|
||||
// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
// )
|
||||
|
||||
// func newNS(n string) v1.Namespace {
|
||||
// return v1.Namespace{ObjectMeta: metav1.ObjectMeta{
|
||||
// Name: n,
|
||||
// }}
|
||||
// }
|
||||
|
||||
// func TestHelpNew(t *testing.T) {
|
||||
// a := view.NewApp(config.NewConfig(ks{}))
|
||||
// v := view.NewHelp()
|
||||
// ctx := context.WithValue(ui.KeyApp, app)
|
||||
// v.Init(ctx)
|
||||
|
||||
// app.SetHints(model.MenuHints{{Mnemonic: "blee", Description: "duh"}})
|
||||
|
||||
// assert.Equal(t, "<blee>", v.GetCell(1, 0).Text)
|
||||
// assert.Equal(t, "duh", v.GetCell(1, 1).Text)
|
||||
// }
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package views
|
||||
package view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package views
|
||||
package view
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type Job struct {
|
||||
*LogResource
|
||||
}
|
||||
|
||||
func NewJob(title, gvr string, list resource.List) ResourceViewer {
|
||||
j := Job{NewLogResource(title, gvr, list)}
|
||||
j.extraActionsFn = j.extraActions
|
||||
j.enterFn = j.showPods
|
||||
|
||||
return &j
|
||||
}
|
||||
|
||||
func (j *Job) extraActions(aa ui.KeyActions) {
|
||||
j.LogResource.extraActions(aa)
|
||||
}
|
||||
|
||||
func (j *Job) showPods(app *App, ns, res, sel string) {
|
||||
ns, n := namespaced(sel)
|
||||
job, err := k8s.NewJob(app.Conn()).Get(ns, n)
|
||||
if err != nil {
|
||||
app.Flash().Err(err)
|
||||
return
|
||||
}
|
||||
|
||||
jo := job.(*batchv1.Job)
|
||||
l, err := metav1.LabelSelectorAsSelector(jo.Spec.Selector)
|
||||
if err != nil {
|
||||
app.Flash().Err(err)
|
||||
return
|
||||
}
|
||||
|
||||
showPods(app, ns, l.String(), "", j.backCmd)
|
||||
}
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type logFrame struct {
|
||||
*tview.Flex
|
||||
|
||||
app *App
|
||||
actions ui.KeyActions
|
||||
backFn ui.ActionHandler
|
||||
}
|
||||
|
||||
func newLogFrame(app *App, backFn ui.ActionHandler) *logFrame {
|
||||
f := logFrame{
|
||||
Flex: tview.NewFlex(),
|
||||
app: app,
|
||||
backFn: backFn,
|
||||
actions: make(ui.KeyActions),
|
||||
}
|
||||
f.SetBorder(true)
|
||||
f.SetBackgroundColor(config.AsColor(app.Styles.Views().Log.BgColor))
|
||||
f.SetBorderPadding(0, 0, 1, 1)
|
||||
f.SetDirection(tview.FlexRow)
|
||||
|
||||
return &f
|
||||
}
|
||||
|
||||
type Log struct {
|
||||
*logFrame
|
||||
|
||||
logs *Details
|
||||
status *statusView
|
||||
ansiWriter io.Writer
|
||||
autoScroll int32
|
||||
path string
|
||||
}
|
||||
|
||||
func NewLog(_ string, app *App, backFn ui.ActionHandler) *Log {
|
||||
l := Log{
|
||||
logFrame: newLogFrame(app, backFn),
|
||||
autoScroll: 1,
|
||||
}
|
||||
|
||||
l.logs = NewDetails(app, backFn)
|
||||
{
|
||||
l.logs.SetBorder(false)
|
||||
l.logs.setCategory("Logs")
|
||||
l.logs.SetDynamicColors(true)
|
||||
l.logs.SetTextColor(config.AsColor(app.Styles.Views().Log.FgColor))
|
||||
l.logs.SetBackgroundColor(config.AsColor(app.Styles.Views().Log.BgColor))
|
||||
l.logs.SetWrap(true)
|
||||
l.logs.SetMaxBuffer(app.Config.K9s.LogBufferSize)
|
||||
}
|
||||
l.ansiWriter = tview.ANSIWriter(l.logs, app.Styles.Views().Log.FgColor, app.Styles.Views().Log.BgColor)
|
||||
l.status = newStatusView(app.Styles)
|
||||
l.AddItem(l.status, 1, 1, false)
|
||||
l.AddItem(l.logs, 0, 1, true)
|
||||
|
||||
l.bindKeys()
|
||||
l.logs.SetInputCapture(l.keyboard)
|
||||
|
||||
return &l
|
||||
}
|
||||
|
||||
func (l *Log) bindKeys() {
|
||||
l.actions = ui.KeyActions{
|
||||
tcell.KeyEscape: ui.NewKeyAction("Back", l.backCmd, true),
|
||||
ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true),
|
||||
ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleScrollCmd, true),
|
||||
ui.KeyG: ui.NewKeyAction("Top", l.topCmd, false),
|
||||
ui.KeyShiftG: ui.NewKeyAction("Bottom", l.bottomCmd, false),
|
||||
ui.KeyF: ui.NewKeyAction("Up", l.pageUpCmd, false),
|
||||
ui.KeyB: ui.NewKeyAction("Down", l.pageDownCmd, false),
|
||||
tcell.KeyCtrlS: ui.NewKeyAction("Save", l.saveCmd, true),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Log) setTitle(path, co string) {
|
||||
var fmat string
|
||||
if co == "" {
|
||||
fmat = skinTitle(fmt.Sprintf(logFmt, path), l.app.Styles.Frame())
|
||||
} else {
|
||||
fmat = skinTitle(fmt.Sprintf(logCoFmt, path, co), l.app.Styles.Frame())
|
||||
}
|
||||
l.path = path
|
||||
l.SetTitle(fmat)
|
||||
}
|
||||
|
||||
// Hints show action hints
|
||||
func (l *Log) Hints() model.MenuHints {
|
||||
return l.actions.Hints()
|
||||
}
|
||||
|
||||
func (l *Log) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
key := evt.Key()
|
||||
if key == tcell.KeyRune {
|
||||
key = tcell.Key(evt.Rune())
|
||||
}
|
||||
if m, ok := l.actions[key]; ok {
|
||||
log.Debug().Msgf(">> LogView handled %s", tcell.KeyNames[key])
|
||||
return m.Action(evt)
|
||||
}
|
||||
|
||||
return evt
|
||||
}
|
||||
|
||||
func (l *Log) log(lines string) {
|
||||
fmt.Fprintln(l.ansiWriter, tview.Escape(lines))
|
||||
log.Debug().Msgf("LOG LINES %d", l.logs.GetLineCount())
|
||||
}
|
||||
|
||||
func (l *Log) flush(index int, buff []string) {
|
||||
if index == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if atomic.LoadInt32(&l.autoScroll) == 1 {
|
||||
l.log(strings.Join(buff[:index], "\n"))
|
||||
l.app.QueueUpdateDraw(func() {
|
||||
l.updateIndicator()
|
||||
l.logs.ScrollToEnd()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Log) updateIndicator() {
|
||||
status := "Off"
|
||||
if l.autoScroll == 1 {
|
||||
status = "On"
|
||||
}
|
||||
l.status.update([]string{fmt.Sprintf("Autoscroll: %s", status)})
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Actions...
|
||||
|
||||
func (l *Log) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if path, err := saveData(l.app.Config.K9s.CurrentCluster, l.path, l.logs.GetText(true)); err != nil {
|
||||
l.app.Flash().Err(err)
|
||||
} else {
|
||||
l.app.Flash().Infof("Log %s saved successfully!", path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureDir(dir string) error {
|
||||
return os.MkdirAll(dir, 0744)
|
||||
}
|
||||
|
||||
func saveData(cluster, name, data string) (string, error) {
|
||||
dir := filepath.Join(config.K9sDumpDir, cluster)
|
||||
if err := ensureDir(dir); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
now := time.Now().UnixNano()
|
||||
fName := fmt.Sprintf("%s-%d.log", strings.Replace(name, "/", "-", -1), now)
|
||||
|
||||
path := filepath.Join(dir, fName)
|
||||
mod := os.O_CREATE | os.O_WRONLY
|
||||
file, err := os.OpenFile(path, mod, 0644)
|
||||
defer func() {
|
||||
if file != nil {
|
||||
file.Close()
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("LogFile create %s", path)
|
||||
return "", nil
|
||||
}
|
||||
if _, err := fmt.Fprintf(file, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func (l *Log) toggleScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if atomic.LoadInt32(&l.autoScroll) == 0 {
|
||||
atomic.StoreInt32(&l.autoScroll, 1)
|
||||
} else {
|
||||
atomic.StoreInt32(&l.autoScroll, 0)
|
||||
}
|
||||
|
||||
if atomic.LoadInt32(&l.autoScroll) == 1 {
|
||||
l.app.Flash().Info("Autoscroll is on.")
|
||||
l.logs.ScrollToEnd()
|
||||
} else {
|
||||
l.logs.LineUp()
|
||||
l.app.Flash().Info("Autoscroll is off.")
|
||||
}
|
||||
l.updateIndicator()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return l.backFn(evt)
|
||||
}
|
||||
|
||||
func (l *Log) topCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
l.app.Flash().Info("Top of logs...")
|
||||
l.logs.ScrollToBeginning()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) bottomCmd(*tcell.EventKey) *tcell.EventKey {
|
||||
l.app.Flash().Info("Bottom of logs...")
|
||||
l.logs.ScrollToEnd()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) pageUpCmd(*tcell.EventKey) *tcell.EventKey {
|
||||
if l.logs.PageUp() {
|
||||
l.app.Flash().Info("Reached Top ...")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) pageDownCmd(*tcell.EventKey) *tcell.EventKey {
|
||||
if l.logs.PageDown() {
|
||||
l.app.Flash().Info("Reached Bottom ...")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey {
|
||||
l.app.Flash().Info("Clearing logs...")
|
||||
l.logs.Clear()
|
||||
l.logs.ScrollTo(0, 0)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
// ContainerFn returns the active container name.
|
||||
type containerFn func() string
|
||||
|
||||
// LogResource represents a loggable resource view.
|
||||
type LogResource struct {
|
||||
*Resource
|
||||
|
||||
containerFn containerFn
|
||||
}
|
||||
|
||||
func NewLogResource(title, gvr string, list resource.List) *LogResource {
|
||||
l := LogResource{
|
||||
Resource: NewResource(title, gvr, list),
|
||||
}
|
||||
l.AddPage("logs", NewLogs(list.GetName(), &l), true, false)
|
||||
|
||||
return &l
|
||||
}
|
||||
|
||||
func (l *LogResource) extraActions(aa ui.KeyActions) {
|
||||
aa[ui.KeyL] = ui.NewKeyAction("Logs", l.logsCmd, true)
|
||||
aa[ui.KeyShiftL] = ui.NewKeyAction("Logs Previous", l.prevLogsCmd, true)
|
||||
}
|
||||
|
||||
func (l *LogResource) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
t := l.masterPage()
|
||||
t.SetSortCol(t.NameColIndex()+col, 0, asc)
|
||||
t.Refresh()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Protocol...
|
||||
|
||||
func (l *LogResource) getList() resource.List {
|
||||
return l.list
|
||||
}
|
||||
|
||||
func (l *LogResource) getSelection() string {
|
||||
if l.path != nil {
|
||||
return *l.path
|
||||
}
|
||||
return l.masterPage().GetSelectedItem()
|
||||
}
|
||||
|
||||
func (l *LogResource) prevLogsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
l.showLogs(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LogResource) logsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
l.showLogs(false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LogResource) showLogs(prev bool) {
|
||||
if !l.masterPage().RowSelected() {
|
||||
return
|
||||
}
|
||||
|
||||
logs := l.GetPrimitive("logs").(*Logs)
|
||||
co := ""
|
||||
if l.containerFn != nil {
|
||||
co = l.containerFn()
|
||||
}
|
||||
logs.reload(co, l, prev)
|
||||
l.switchPage("logs")
|
||||
}
|
||||
|
||||
func (l *LogResource) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
// Reset namespace to what it was
|
||||
l.app.Config.SetActiveNamespace(l.list.GetNamespace())
|
||||
l.app.inject(l)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package view_test
|
||||
|
||||
// import (
|
||||
// "bytes"
|
||||
// "fmt"
|
||||
// "testing"
|
||||
|
||||
// "github.com/derailed/k9s/internal/config"
|
||||
// "github.com/derailed/k9s/internal/view"
|
||||
// "github.com/derailed/tview"
|
||||
// "github.com/stretchr/testify/assert"
|
||||
// )
|
||||
|
||||
// func TestAnsi(t *testing.T) {
|
||||
// buff := bytes.NewBufferString("")
|
||||
// w := tview.ANSIWriter(buff, "white", "black")
|
||||
// fmt.Fprintf(w, "[YELLOW] ok")
|
||||
// assert.Equal(t, "[YELLOW] ok", buff.String())
|
||||
|
||||
// v := tview.NewTextView()
|
||||
// v.SetDynamicColors(true)
|
||||
// aw := tview.ANSIWriter(v, "white", "black")
|
||||
// s := "[2019-03-27T15:05:15,246][INFO ][o.e.c.r.a.AllocationService] [es-0] Cluster health status changed from [YELLOW] to [GREEN] (reason: [shards started [[.monitoring-es-6-2019.03.27][0]]"
|
||||
// fmt.Fprintf(aw, s)
|
||||
// assert.Equal(t, s+"\n", v.GetText(false))
|
||||
// }
|
||||
|
||||
// func TestLogFlush(t *testing.T) {
|
||||
// v := view.NewLog("Logs", NewApp(config.NewConfig(ks{})), nil)
|
||||
// v.flush(2, []string{"blee", "bozo"})
|
||||
|
||||
// v.toggleScrollCmd(nil)
|
||||
// assert.Equal(t, "blee\nbozo\n", v.logs.GetText(true))
|
||||
// assert.Equal(t, " Autoscroll: Off ", v.status.GetText(true))
|
||||
// v.toggleScrollCmd(nil)
|
||||
// assert.Equal(t, " Autoscroll: On ", v.status.GetText(true))
|
||||
// }
|
||||
|
||||
// func TestLogViewSave(t *testing.T) {
|
||||
// v := newLogView("Logs", NewApp(config.NewConfig(ks{})), nil)
|
||||
// v.flush(2, []string{"blee", "bozo"})
|
||||
// v.path = "k9s-test"
|
||||
// dir := filepath.Join(config.K9sDumpDir, v.app.Config.K9s.CurrentCluster)
|
||||
// c1, _ := ioutil.ReadDir(dir)
|
||||
// v.saveCmd(nil)
|
||||
// c2, _ := ioutil.ReadDir(dir)
|
||||
// assert.Equal(t, len(c2), len(c1)+1)
|
||||
// }
|
||||
|
||||
// func TestLogViewNav(t *testing.T) {
|
||||
// v := newLogView("Logs", NewApp(config.NewConfig(ks{})), nil)
|
||||
// var buff []string
|
||||
// v.autoScroll = 1
|
||||
// for i := 0; i < 100; i++ {
|
||||
// buff = append(buff, fmt.Sprintf("line-%d\n", i))
|
||||
// }
|
||||
// v.flush(100, buff)
|
||||
|
||||
// v.topCmd(nil)
|
||||
// r, _ := v.logs.GetScrollOffset()
|
||||
// assert.Equal(t, 0, r)
|
||||
// v.pageDownCmd(nil)
|
||||
// r, _ = v.logs.GetScrollOffset()
|
||||
// assert.Equal(t, 0, r)
|
||||
// v.pageUpCmd(nil)
|
||||
// r, _ = v.logs.GetScrollOffset()
|
||||
// assert.Equal(t, 0, r)
|
||||
// v.bottomCmd(nil)
|
||||
// r, _ = v.logs.GetScrollOffset()
|
||||
// assert.Equal(t, 0, r)
|
||||
// }
|
||||
|
||||
// func TestLogViewClear(t *testing.T) {
|
||||
// v := newLogView("Logs", NewApp(config.NewConfig(ks{})), nil)
|
||||
// v.flush(2, []string{"blee", "bozo"})
|
||||
|
||||
// v.toggleScrollCmd(nil)
|
||||
// assert.Equal(t, "blee\nbozo\n", v.logs.GetText(true))
|
||||
// v.clearCmd(nil)
|
||||
// assert.Equal(t, "", v.logs.GetText(true))
|
||||
// }
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
package views
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/tview"
|
||||
|
|
@ -23,98 +24,97 @@ const (
|
|||
type (
|
||||
masterView interface {
|
||||
backFn() ui.ActionHandler
|
||||
appView() *appView
|
||||
App() *App
|
||||
}
|
||||
|
||||
logsView struct {
|
||||
// Logs presents a collection of logs.
|
||||
Logs struct {
|
||||
*tview.Pages
|
||||
|
||||
app *appView
|
||||
app *App
|
||||
parent loggable
|
||||
actions ui.KeyActions
|
||||
cancelFunc context.CancelFunc
|
||||
}
|
||||
)
|
||||
|
||||
func newLogsView(title string, app *appView, parent loggable) *logsView {
|
||||
v := logsView{
|
||||
app: app,
|
||||
// NewLogs returns a new logs viewer.
|
||||
func NewLogs(title string, parent loggable) *Logs {
|
||||
return &Logs{
|
||||
Pages: tview.NewPages(),
|
||||
parent: parent,
|
||||
}
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
// Protocol...
|
||||
|
||||
func (v *logsView) reload(co string, parent loggable, prevLogs bool) {
|
||||
v.parent = parent
|
||||
v.deletePage()
|
||||
v.AddPage("logs", newLogView(co, v.app, v.backCmd), true, true)
|
||||
v.load(co, prevLogs)
|
||||
func (l *Logs) reload(co string, parent loggable, prevLogs bool) {
|
||||
l.parent = parent
|
||||
l.deletePage()
|
||||
l.AddPage("logs", NewLog(co, l.app, l.backCmd), true, true)
|
||||
l.load(co, prevLogs)
|
||||
}
|
||||
|
||||
// SetActions to handle keyboard events.
|
||||
func (v *logsView) setActions(aa ui.KeyActions) {
|
||||
v.actions = aa
|
||||
func (l *Logs) setActions(aa ui.KeyActions) {
|
||||
l.actions = aa
|
||||
}
|
||||
|
||||
// Hints show action hints
|
||||
func (v *logsView) Hints() ui.Hints {
|
||||
l := v.CurrentPage().Item.(*logView)
|
||||
return l.actions.Hints()
|
||||
func (l *Logs) Hints() model.MenuHints {
|
||||
v := l.CurrentPage().Item.(*Log)
|
||||
return v.actions.Hints()
|
||||
}
|
||||
|
||||
func (v *logsView) backFn() ui.ActionHandler {
|
||||
return v.backCmd
|
||||
func (l *Logs) backFn() ui.ActionHandler {
|
||||
return l.backCmd
|
||||
}
|
||||
|
||||
func (v *logsView) deletePage() {
|
||||
v.RemovePage("logs")
|
||||
func (l *Logs) deletePage() {
|
||||
l.RemovePage("logs")
|
||||
}
|
||||
|
||||
func (v *logsView) stop() {
|
||||
if v.cancelFunc == nil {
|
||||
func (l *Logs) stop() {
|
||||
if l.cancelFunc == nil {
|
||||
return
|
||||
}
|
||||
v.cancelFunc()
|
||||
l.cancelFunc()
|
||||
log.Debug().Msgf("Canceling logs...")
|
||||
v.cancelFunc = nil
|
||||
l.cancelFunc = nil
|
||||
}
|
||||
|
||||
func (v *logsView) load(container string, prevLogs bool) {
|
||||
if err := v.doLoad(v.parent.getSelection(), container, prevLogs); err != nil {
|
||||
v.app.Flash().Err(err)
|
||||
l := v.CurrentPage().Item.(*logView)
|
||||
func (l *Logs) load(container string, prevLogs bool) {
|
||||
if err := l.doLoad(l.parent.getSelection(), container, prevLogs); err != nil {
|
||||
l.app.Flash().Err(err)
|
||||
l := l.CurrentPage().Item.(*Log)
|
||||
l.log("😂 Doh! No logs are available at this time. Check again later on...")
|
||||
return
|
||||
}
|
||||
v.app.SetFocus(v)
|
||||
l.app.SetFocus(l)
|
||||
}
|
||||
|
||||
func (v *logsView) doLoad(path, co string, prevLogs bool) error {
|
||||
v.stop()
|
||||
func (l *Logs) doLoad(path, co string, prevLogs bool) error {
|
||||
l.stop()
|
||||
|
||||
l := v.CurrentPage().Item.(*logView)
|
||||
l.logs.Clear()
|
||||
l.setTitle(path, co)
|
||||
v := l.CurrentPage().Item.(*Log)
|
||||
v.logs.Clear()
|
||||
v.setTitle(path, co)
|
||||
|
||||
var ctx context.Context
|
||||
ctx = context.WithValue(context.Background(), resource.IKey("informer"), v.app.informer)
|
||||
ctx, v.cancelFunc = context.WithCancel(ctx)
|
||||
ctx = context.WithValue(context.Background(), resource.IKey("informer"), l.app.informer)
|
||||
ctx, l.cancelFunc = context.WithCancel(ctx)
|
||||
|
||||
c := make(chan string, 10)
|
||||
go updateLogs(ctx, c, l, logBuffSize)
|
||||
go updateLogs(ctx, c, v, logBuffSize)
|
||||
|
||||
res, ok := v.parent.getList().Resource().(resource.Tailable)
|
||||
res, ok := l.parent.getList().Resource().(resource.Tailable)
|
||||
if !ok {
|
||||
close(c)
|
||||
return fmt.Errorf("Resource %T is not tailable", v.parent.getList().Resource())
|
||||
return fmt.Errorf("Resource %T is not tailable", l.parent.getList().Resource())
|
||||
}
|
||||
|
||||
if err := res.Logs(ctx, c, v.logOpts(path, co, prevLogs)); err != nil {
|
||||
v.cancelFunc()
|
||||
if err := res.Logs(ctx, c, l.logOpts(path, co, prevLogs)); err != nil {
|
||||
l.cancelFunc()
|
||||
close(c)
|
||||
return err
|
||||
}
|
||||
|
|
@ -122,7 +122,7 @@ func (v *logsView) doLoad(path, co string, prevLogs bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (v *logsView) logOpts(path, co string, prevLogs bool) resource.LogOptions {
|
||||
func (l *Logs) logOpts(path, co string, prevLogs bool) resource.LogOptions {
|
||||
ns, po := namespaced(path)
|
||||
return resource.LogOptions{
|
||||
Fqn: resource.Fqn{
|
||||
|
|
@ -130,12 +130,12 @@ func (v *logsView) logOpts(path, co string, prevLogs bool) resource.LogOptions {
|
|||
Name: po,
|
||||
Container: co,
|
||||
},
|
||||
Lines: int64(v.app.Config.K9s.LogRequestSize),
|
||||
Lines: int64(l.app.Config.K9s.LogRequestSize),
|
||||
Previous: prevLogs,
|
||||
}
|
||||
}
|
||||
|
||||
func updateLogs(ctx context.Context, c <-chan string, l *logView, buffSize int) {
|
||||
func updateLogs(ctx context.Context, c <-chan string, l *Log, buffSize int) {
|
||||
defer func() {
|
||||
log.Debug().Msgf("updateLogs view bailing out!")
|
||||
}()
|
||||
|
|
@ -169,9 +169,9 @@ func updateLogs(ctx context.Context, c <-chan string, l *logView, buffSize int)
|
|||
// ----------------------------------------------------------------------------
|
||||
// Actions...
|
||||
|
||||
func (v *logsView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
v.stop()
|
||||
v.parent.switchPage("master")
|
||||
func (l *Logs) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
l.stop()
|
||||
l.parent.switchPage("master")
|
||||
|
||||
return evt
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package view
|
||||
|
||||
// func TestUpdateLogs(t *testing.T) {
|
||||
// v := newLogView("test", NewApp(config.NewConfig(ks{})), nil)
|
||||
|
||||
// var wg sync.WaitGroup
|
||||
// wg.Add(1)
|
||||
// c := make(chan string, 10)
|
||||
// go func() {
|
||||
// defer wg.Done()
|
||||
// updateLogs(context.Background(), c, v, 10)
|
||||
// }()
|
||||
|
||||
// for i := 0; i < 500; i++ {
|
||||
// c <- fmt.Sprintf("log %d", i)
|
||||
// }
|
||||
// close(c)
|
||||
// wg.Wait()
|
||||
|
||||
// assert.Equal(t, 500, v.logs.GetLineCount())
|
||||
// }
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
)
|
||||
|
||||
// MasterDetail presents a master-detail viewer.
|
||||
type MasterDetail struct {
|
||||
*PageStack
|
||||
|
||||
enterFn enterFn
|
||||
extraActionsFn func(ui.KeyActions)
|
||||
details *Details
|
||||
}
|
||||
|
||||
// NewMasterDetail returns a new master-detail viewer.
|
||||
func NewMasterDetail() *MasterDetail {
|
||||
return &MasterDetail{
|
||||
PageStack: NewPageStack(),
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes the viewer.
|
||||
func (m *MasterDetail) Init(ctx context.Context) {
|
||||
m.PageStack.Init(ctx)
|
||||
|
||||
t := NewTable("master")
|
||||
t.Init(ctx)
|
||||
m.Push(t)
|
||||
|
||||
m.details = NewDetails(m.app, nil)
|
||||
m.details.Init(ctx)
|
||||
}
|
||||
|
||||
func (m *MasterDetail) setExtraActionsFn(f ActionsFunc) {
|
||||
m.extraActionsFn = f
|
||||
}
|
||||
|
||||
// Protocol...
|
||||
|
||||
func (m *MasterDetail) setEnterFn(f enterFn) {
|
||||
m.enterFn = f
|
||||
}
|
||||
|
||||
func (m *MasterDetail) showMaster() {
|
||||
m.Show("table")
|
||||
}
|
||||
|
||||
func (m *MasterDetail) masterPage() *Table {
|
||||
return m.GetPrimitive("table").(*Table)
|
||||
}
|
||||
|
||||
func (m *MasterDetail) showDetails() {
|
||||
m.Push(m.details)
|
||||
}
|
||||
|
||||
func (m *MasterDetail) detailsPage() *Details {
|
||||
return m.details
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Actions...
|
||||
|
||||
func (m *MasterDetail) defaultActions(aa ui.KeyActions) {
|
||||
aa[ui.KeyHelp] = ui.NewKeyAction("Help", noopCmd, false)
|
||||
aa[ui.KeyP] = ui.NewKeyAction("Previous", m.app.PrevCmd, false)
|
||||
|
||||
if m.extraActionsFn != nil {
|
||||
m.extraActionsFn(aa)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package view
|
||||
|
||||
// import (
|
||||
// "testing"
|
||||
|
||||
// "github.com/stretchr/testify/assert"
|
||||
// )
|
||||
|
||||
// func TestNSCleanser(t *testing.T) {
|
||||
// var v namespaceView
|
||||
|
||||
// uu := []struct {
|
||||
// s, e string
|
||||
// }{
|
||||
// {"fred", "fred"},
|
||||
// {"fred+", "fred"},
|
||||
// {"fred(*)", "fred"},
|
||||
// {"fred+(*)", "fred"},
|
||||
// {"fred-blee+(*)", "fred-blee"},
|
||||
// {"fred1-blee2+(*)", "fred1-blee2"},
|
||||
// {"fred(𝜟)", "fred"},
|
||||
// }
|
||||
|
||||
// for _, u := range uu {
|
||||
// assert.Equal(t, u.e, v.cleanser(u.s))
|
||||
// }
|
||||
// }
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
// Node represents a node view.
|
||||
type Node struct {
|
||||
*Resource
|
||||
}
|
||||
|
||||
// NewNode returns a new node view.
|
||||
func NewNode(title, gvr string, list resource.List) ResourceViewer {
|
||||
n := Node{
|
||||
Resource: NewResource(title, gvr, list),
|
||||
}
|
||||
n.extraActionsFn = n.extraActions
|
||||
n.enterFn = n.showPods
|
||||
|
||||
return &n
|
||||
}
|
||||
|
||||
func (n *Node) extraActions(aa ui.KeyActions) {
|
||||
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", n.sortColCmd(7, false), false)
|
||||
aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", n.sortColCmd(8, false), false)
|
||||
aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", n.sortColCmd(9, false), false)
|
||||
aa[ui.KeyShiftZ] = ui.NewKeyAction("Sort MEM%", n.sortColCmd(10, false), false)
|
||||
}
|
||||
|
||||
func (n *Node) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
t := n.masterPage()
|
||||
t.SetSortCol(t.NameColIndex()+col, 0, asc)
|
||||
t.Refresh()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) showPods(app *App, _, _, sel string) {
|
||||
showPods(app, "", "", "spec.nodeName="+sel, n.backCmd)
|
||||
}
|
||||
|
||||
func (n *Node) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
// BOZO!!
|
||||
// n.App.inject(v)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func showPods(app *App, ns, labelSel, fieldSel string, a ui.ActionHandler) {
|
||||
app.switchNS(ns)
|
||||
|
||||
list := resource.NewPodList(app.Conn(), ns)
|
||||
list.SetLabelSelector(labelSel)
|
||||
list.SetFieldSelector(fieldSel)
|
||||
|
||||
v := NewPod("Pod", "v1/pods", list)
|
||||
v.setColorerFn(podColorer)
|
||||
v.masterPage().AddActions(ui.KeyActions{
|
||||
tcell.KeyEsc: ui.NewKeyAction("Back", a, true),
|
||||
})
|
||||
app.Config.SetActiveNamespace(ns)
|
||||
app.inject(v)
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
const (
|
||||
favNSIndicator = "+"
|
||||
defaultNSIndicator = "(*)"
|
||||
deltaNSIndicator = "(𝜟)"
|
||||
)
|
||||
|
||||
var nsCleanser = regexp.MustCompile(`(\w+)[+|(*)|(𝜟)]*`)
|
||||
|
||||
// Namespace represents a namespace viewer.
|
||||
type Namespace struct {
|
||||
*Resource
|
||||
}
|
||||
|
||||
// NewNamespace returns a new viewer
|
||||
func NewNamespace(title, gvr string, list resource.List) ResourceViewer {
|
||||
n := Namespace{
|
||||
Resource: NewResource(title, gvr, list),
|
||||
}
|
||||
n.extraActionsFn = n.extraActions
|
||||
n.masterPage().SetSelectedFn(n.cleanser)
|
||||
n.decorateFn = n.decorate
|
||||
n.enterFn = n.switchNs
|
||||
|
||||
return &n
|
||||
}
|
||||
|
||||
func (n *Namespace) extraActions(aa ui.KeyActions) {
|
||||
aa[ui.KeyU] = ui.NewKeyAction("Use", n.useNsCmd, true)
|
||||
}
|
||||
|
||||
func (n *Namespace) switchNs(app *App, _, res, sel string) {
|
||||
n.useNamespace(sel)
|
||||
app.gotoResource("po", true)
|
||||
}
|
||||
|
||||
func (n *Namespace) useNsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !n.masterPage().RowSelected() {
|
||||
return evt
|
||||
}
|
||||
n.useNamespace(n.masterPage().GetSelectedItem())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Namespace) useNamespace(ns string) {
|
||||
if err := n.app.Config.SetActiveNamespace(ns); err != nil {
|
||||
n.app.Flash().Err(err)
|
||||
} else {
|
||||
n.app.Flash().Infof("Namespace %s is now active!", ns)
|
||||
}
|
||||
n.app.Config.Save()
|
||||
n.app.startInformer(ns)
|
||||
}
|
||||
|
||||
func (*Namespace) cleanser(s string) string {
|
||||
return nsCleanser.ReplaceAllString(s, `$1`)
|
||||
}
|
||||
|
||||
func (n *Namespace) decorate(data resource.TableData) resource.TableData {
|
||||
if _, ok := data.Rows[resource.AllNamespaces]; !ok {
|
||||
if err := n.app.Conn().CheckNSAccess(""); err == nil {
|
||||
data.Rows[resource.AllNamespace] = &resource.RowEvent{
|
||||
Action: resource.Unchanged,
|
||||
Fields: resource.Row{resource.AllNamespace, "Active", "0"},
|
||||
Deltas: resource.Row{"", "", ""},
|
||||
}
|
||||
}
|
||||
}
|
||||
for k, r := range data.Rows {
|
||||
if config.InList(n.app.Config.FavNamespaces(), k) {
|
||||
r.Fields[0] += "+"
|
||||
r.Action = resource.Unchanged
|
||||
}
|
||||
if n.app.Config.ActiveNamespace() == k {
|
||||
r.Fields[0] += "(*)"
|
||||
r.Action = resource.Unchanged
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type PageStack struct {
|
||||
*ui.Pages
|
||||
|
||||
app *App
|
||||
}
|
||||
|
||||
func NewPageStack() *PageStack {
|
||||
return &PageStack{
|
||||
Pages: ui.NewPages(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PageStack) Init(ctx context.Context) {
|
||||
p.app = ctx.Value(ui.KeyApp).(*App)
|
||||
|
||||
p.Pages.SetChangedFunc(func() {
|
||||
log.Debug().Msgf(">>>>>PS CHNGED<<<<<")
|
||||
p.DumpStack()
|
||||
active := p.CurrentPage()
|
||||
if active == nil {
|
||||
return
|
||||
}
|
||||
c := active.Item.(model.Component)
|
||||
log.Debug().Msgf("-------Page activated %#v", active)
|
||||
p.app.Hint.SetHints(c.Hints())
|
||||
})
|
||||
|
||||
p.Pages.SetTitle("Fuck!")
|
||||
p.Stack.AddListener(p)
|
||||
}
|
||||
|
||||
func (p *PageStack) StackPushed(c model.Component) {
|
||||
ctx := context.WithValue(context.Background(), ui.KeyApp, p.app)
|
||||
c.Init(ctx)
|
||||
p.app.SetFocus(c)
|
||||
p.app.Hint.SetHints(c.Hints())
|
||||
}
|
||||
|
||||
func (p *PageStack) StackPopped(o, top model.Component) {
|
||||
o.Stop()
|
||||
p.StackTop(top)
|
||||
}
|
||||
|
||||
func (p *PageStack) StackTop(top model.Component) {
|
||||
if top == nil {
|
||||
return
|
||||
}
|
||||
top.Start()
|
||||
p.app.SetFocus(top)
|
||||
p.app.Hint.SetHints(top.Hints())
|
||||
}
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/k9s/internal/watch"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
containerFmt = "[fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])"
|
||||
shellCheck = "command -v bash >/dev/null && exec bash || exec sh"
|
||||
)
|
||||
|
||||
type loggable interface {
|
||||
getSelection() string
|
||||
getList() resource.List
|
||||
switchPage(n string)
|
||||
}
|
||||
|
||||
// Pod represents a pod viewer.
|
||||
type Pod struct {
|
||||
*Resource
|
||||
}
|
||||
|
||||
// NewPod returns a new viewer.
|
||||
func NewPod(title, gvr string, list resource.List) ResourceViewer {
|
||||
p := Pod{
|
||||
Resource: NewResource(title, gvr, list),
|
||||
}
|
||||
p.extraActionsFn = p.extraActions
|
||||
p.enterFn = p.listContainers
|
||||
|
||||
picker := newSelectList(&p)
|
||||
{
|
||||
picker.setActions(ui.KeyActions{
|
||||
tcell.KeyEscape: {Description: "Back", Action: p.backCmd, Visible: true},
|
||||
})
|
||||
}
|
||||
p.AddPage("picker", picker, true, false)
|
||||
p.AddPage("logs", NewLogs(list.GetName(), &p), true, false)
|
||||
|
||||
return &p
|
||||
}
|
||||
|
||||
func (p *Pod) extraActions(aa ui.KeyActions) {
|
||||
aa[tcell.KeyCtrlK] = ui.NewKeyAction("Kill", p.killCmd, true)
|
||||
aa[ui.KeyS] = ui.NewKeyAction("Shell", p.shellCmd, true)
|
||||
|
||||
aa[ui.KeyL] = ui.NewKeyAction("Logs", p.logsCmd, true)
|
||||
aa[ui.KeyShiftL] = ui.NewKeyAction("Logs Previous", p.prevLogsCmd, true)
|
||||
|
||||
aa[ui.KeyShiftR] = ui.NewKeyAction("Sort Ready", p.sortColCmd(1, false), false)
|
||||
aa[ui.KeyShiftS] = ui.NewKeyAction("Sort Status", p.sortColCmd(2, true), false)
|
||||
aa[ui.KeyShiftT] = ui.NewKeyAction("Sort Restart", p.sortColCmd(3, false), false)
|
||||
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", p.sortColCmd(4, false), false)
|
||||
aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", p.sortColCmd(5, false), false)
|
||||
aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", p.sortColCmd(6, false), false)
|
||||
aa[ui.KeyShiftZ] = ui.NewKeyAction("Sort MEM%", p.sortColCmd(7, false), false)
|
||||
aa[ui.KeyShiftD] = ui.NewKeyAction("Sort IP", p.sortColCmd(8, true), false)
|
||||
aa[ui.KeyShiftO] = ui.NewKeyAction("Sort Node", p.sortColCmd(9, true), false)
|
||||
}
|
||||
|
||||
func (p *Pod) listContainers(app *App, _, res, sel string) {
|
||||
po, err := p.app.informer.Get(watch.PodIndex, sel, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
app.Flash().Errf("Unable to retrieve pods %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
pod := po.(*v1.Pod)
|
||||
list := resource.NewContainerList(app.Conn(), pod)
|
||||
title := skinTitle(fmt.Sprintf(containerFmt, "Container", sel), app.Styles.Frame())
|
||||
|
||||
// Stop my updater
|
||||
if p.cancelFn != nil {
|
||||
p.cancelFn()
|
||||
}
|
||||
|
||||
// Span child view
|
||||
v := NewContainer(title, list, fqn(pod.Namespace, pod.Name))
|
||||
p.app.inject(v)
|
||||
}
|
||||
|
||||
// Protocol...
|
||||
|
||||
func (p *Pod) getList() resource.List {
|
||||
return p.list
|
||||
}
|
||||
|
||||
func (p *Pod) getSelection() string {
|
||||
return p.masterPage().GetSelectedItem()
|
||||
}
|
||||
|
||||
func (p *Pod) killCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !p.masterPage().RowSelected() {
|
||||
return evt
|
||||
}
|
||||
sel := p.masterPage().GetSelectedItems()
|
||||
p.masterPage().ShowDeleted()
|
||||
for _, res := range sel {
|
||||
p.app.Flash().Infof("Delete resource %s %s", p.list.GetName(), res)
|
||||
if err := p.list.Resource().Delete(res, true, false); err != nil {
|
||||
p.app.Flash().Errf("Delete failed with %s", err)
|
||||
} else {
|
||||
deletePortForward(p.app.forwarders, res)
|
||||
}
|
||||
}
|
||||
p.refresh()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Pod) logsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if p.viewLogs(false) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return evt
|
||||
}
|
||||
|
||||
func (p *Pod) prevLogsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if p.viewLogs(true) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return evt
|
||||
}
|
||||
|
||||
func (p *Pod) viewLogs(prev bool) bool {
|
||||
if !p.masterPage().RowSelected() {
|
||||
return false
|
||||
}
|
||||
p.showLogs(p.masterPage().GetSelectedItem(), "", p, prev)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Pod) showLogs(path, co string, parent loggable, prev bool) {
|
||||
l := p.GetPrimitive("logs").(*Logs)
|
||||
l.reload(co, parent, prev)
|
||||
p.switchPage("logs")
|
||||
}
|
||||
|
||||
func (p *Pod) shellCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !p.masterPage().RowSelected() {
|
||||
return evt
|
||||
}
|
||||
|
||||
sel := p.masterPage().GetSelectedItem()
|
||||
cc, err := fetchContainers(p.list, sel, false)
|
||||
if err != nil {
|
||||
p.app.Flash().Errf("Unable to retrieve containers %s", err)
|
||||
return evt
|
||||
}
|
||||
if len(cc) == 1 {
|
||||
p.shellIn(sel, "")
|
||||
return nil
|
||||
}
|
||||
picker := p.GetPrimitive("picker").(*selectList)
|
||||
picker.populate(cc)
|
||||
picker.SetSelectedFunc(func(i int, t, d string, r rune) {
|
||||
p.shellIn(sel, t)
|
||||
})
|
||||
p.switchPage("picker")
|
||||
|
||||
return evt
|
||||
}
|
||||
|
||||
func (p *Pod) shellIn(path, co string) {
|
||||
p.Stop()
|
||||
shellIn(p.app, path, co)
|
||||
p.Start()
|
||||
}
|
||||
|
||||
func (p *Pod) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
t := p.masterPage()
|
||||
t.SetSortCol(t.NameColIndex()+col, 0, asc)
|
||||
t.Refresh()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func fetchContainers(l resource.List, po string, includeInit bool) ([]string, error) {
|
||||
if len(po) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
return l.Resource().(resource.Containers).Containers(po, includeInit)
|
||||
}
|
||||
|
||||
func shellIn(a *App, path, co string) {
|
||||
args := computeShellArgs(path, co, a.Config.K9s.CurrentContext, a.Conn().Config().Flags().KubeConfig)
|
||||
log.Debug().Msgf("Shell args %v", args)
|
||||
runK(true, a, args...)
|
||||
}
|
||||
|
||||
func computeShellArgs(path, co, context string, kcfg *string) []string {
|
||||
args := make([]string, 0, 15)
|
||||
args = append(args, "exec", "-it")
|
||||
args = append(args, "--context", context)
|
||||
ns, po := namespaced(path)
|
||||
args = append(args, "-n", ns)
|
||||
args = append(args, po)
|
||||
if kcfg != nil && *kcfg != "" {
|
||||
args = append(args, "--kubeconfig", *kcfg)
|
||||
}
|
||||
if co != "" {
|
||||
args = append(args, "-c", co)
|
||||
}
|
||||
|
||||
return append(args, "--", "sh", "-c", shellCheck)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package views
|
||||
package view
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package view_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/view"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPodNew(t *testing.T) {
|
||||
po := view.NewPod("test", "blee", resource.NewPodList(nil, ""))
|
||||
|
||||
assert.Equal(t, "po", po.Name())
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
package views
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/gdamore/tcell"
|
||||
|
|
@ -22,10 +23,10 @@ type (
|
|||
ns, role string
|
||||
}
|
||||
|
||||
policyView struct {
|
||||
*tableView
|
||||
// Policy presents a RBAC policy viewer.
|
||||
Policy struct {
|
||||
*Table
|
||||
|
||||
current ui.Igniter
|
||||
cancel context.CancelFunc
|
||||
subjectKind string
|
||||
subjectName string
|
||||
|
|
@ -33,101 +34,110 @@ type (
|
|||
}
|
||||
)
|
||||
|
||||
func newPolicyView(app *appView, subject, name string) *policyView {
|
||||
v := policyView{}
|
||||
{
|
||||
v.subjectKind, v.subjectName = mapSubject(subject), name
|
||||
v.tableView = newTableView(app, v.getTitle())
|
||||
v.SetColorerFn(rbacColorer)
|
||||
v.current = app.Frame().GetPrimitive("main").(ui.Igniter)
|
||||
v.bindKeys()
|
||||
}
|
||||
// NewPolicy returns a new viewer.
|
||||
func NewPolicy(app *App, subject, name string) *Policy {
|
||||
p := Policy{}
|
||||
p.subjectKind, p.subjectName = mapSubject(subject), name
|
||||
p.Table = NewTable(p.getTitle())
|
||||
p.SetColorerFn(rbacColorer)
|
||||
p.bindKeys()
|
||||
|
||||
return &v
|
||||
return &p
|
||||
}
|
||||
|
||||
// Init the view.
|
||||
func (v *policyView) Init(c context.Context, ns string) {
|
||||
v.SetSortCol(1, len(rbacHeader), false)
|
||||
func (p *Policy) Init(ctx context.Context) {
|
||||
p.Table.Init(ctx)
|
||||
|
||||
ctx, cancel := context.WithCancel(c)
|
||||
v.cancel = cancel
|
||||
p.SetSortCol(1, len(rbacHeader), false)
|
||||
p.Start()
|
||||
p.refresh()
|
||||
p.SelectRow(1, true)
|
||||
}
|
||||
|
||||
func (p *Policy) Name() string {
|
||||
return "policy"
|
||||
}
|
||||
|
||||
func (p *Policy) Start() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
p.cancel = cancel
|
||||
go func(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(time.Duration(v.app.Config.K9s.GetRefreshRate()) * time.Second):
|
||||
v.refresh()
|
||||
v.app.Draw()
|
||||
case <-time.After(time.Duration(p.app.Config.K9s.GetRefreshRate()) * time.Second):
|
||||
p.refresh()
|
||||
p.app.Draw()
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
|
||||
v.refresh()
|
||||
v.SelectRow(1, true)
|
||||
v.app.SetFocus(v)
|
||||
}
|
||||
|
||||
func (v *policyView) bindKeys() {
|
||||
v.RmAction(ui.KeyShiftA)
|
||||
func (p *Policy) Stop() {
|
||||
if p.cancel != nil {
|
||||
p.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
v.SetActions(ui.KeyActions{
|
||||
tcell.KeyEscape: ui.NewKeyAction("Reset", v.resetCmd, false),
|
||||
ui.KeySlash: ui.NewKeyAction("Filter", v.activateCmd, false),
|
||||
ui.KeyP: ui.NewKeyAction("Previous", v.app.prevCmd, false),
|
||||
ui.KeyShiftS: ui.NewKeyAction("Sort Namespace", v.SortColCmd(0), false),
|
||||
ui.KeyShiftN: ui.NewKeyAction("Sort Name", v.SortColCmd(1), false),
|
||||
ui.KeyShiftO: ui.NewKeyAction("Sort Group", v.SortColCmd(2), false),
|
||||
ui.KeyShiftB: ui.NewKeyAction("Sort Binding", v.SortColCmd(3), false),
|
||||
func (p *Policy) bindKeys() {
|
||||
p.RmAction(ui.KeyShiftA)
|
||||
|
||||
p.AddActions(ui.KeyActions{
|
||||
tcell.KeyEscape: ui.NewKeyAction("Reset", p.resetCmd, false),
|
||||
ui.KeySlash: ui.NewKeyAction("Filter", p.activateCmd, false),
|
||||
ui.KeyP: ui.NewKeyAction("Previous", p.app.PrevCmd, false),
|
||||
ui.KeyShiftS: ui.NewKeyAction("Sort Namespace", p.SortColCmd(0), false),
|
||||
ui.KeyShiftN: ui.NewKeyAction("Sort Name", p.SortColCmd(1), false),
|
||||
ui.KeyShiftO: ui.NewKeyAction("Sort Group", p.SortColCmd(2), false),
|
||||
ui.KeyShiftB: ui.NewKeyAction("Sort Binding", p.SortColCmd(3), false),
|
||||
})
|
||||
}
|
||||
|
||||
func (v *policyView) getTitle() string {
|
||||
return fmt.Sprintf(rbacTitleFmt, policyTitle, v.subjectKind+":"+v.subjectName)
|
||||
func (p *Policy) getTitle() string {
|
||||
return fmt.Sprintf(rbacTitleFmt, policyTitle, p.subjectKind+":"+p.subjectName)
|
||||
}
|
||||
|
||||
func (v *policyView) refresh() {
|
||||
data, err := v.reconcile()
|
||||
func (p *Policy) refresh() {
|
||||
data, err := p.reconcile()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Refresh for %s:%s", v.subjectKind, v.subjectName)
|
||||
v.app.Flash().Err(err)
|
||||
log.Error().Err(err).Msgf("Refresh for %s:%s", p.subjectKind, p.subjectName)
|
||||
p.app.Flash().Err(err)
|
||||
}
|
||||
v.Update(data)
|
||||
p.Update(data)
|
||||
}
|
||||
|
||||
func (v *policyView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !v.SearchBuff().Empty() {
|
||||
v.SearchBuff().Reset()
|
||||
func (p *Policy) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !p.SearchBuff().Empty() {
|
||||
p.SearchBuff().Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
return v.backCmd(evt)
|
||||
return p.backCmd(evt)
|
||||
}
|
||||
|
||||
func (v *policyView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if v.cancel != nil {
|
||||
v.cancel()
|
||||
func (p *Policy) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if p.cancel != nil {
|
||||
p.cancel()
|
||||
}
|
||||
|
||||
if v.SearchBuff().IsActive() {
|
||||
v.SearchBuff().Reset()
|
||||
if p.SearchBuff().IsActive() {
|
||||
p.SearchBuff().Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
v.app.inject(v.current)
|
||||
|
||||
return nil
|
||||
return p.app.PrevCmd(evt)
|
||||
}
|
||||
|
||||
func (v *policyView) Hints() ui.Hints {
|
||||
return v.Hints()
|
||||
func (p *Policy) Hints() model.MenuHints {
|
||||
return p.Hints()
|
||||
}
|
||||
|
||||
func (v *policyView) reconcile() (resource.TableData, error) {
|
||||
func (p *Policy) reconcile() (resource.TableData, error) {
|
||||
var table resource.TableData
|
||||
|
||||
evts, errs := v.clusterPolicies()
|
||||
evts, errs := p.clusterPolicies()
|
||||
if len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
log.Error().Err(err).Msg("Unable to find cluster policies")
|
||||
|
|
@ -135,7 +145,7 @@ func (v *policyView) reconcile() (resource.TableData, error) {
|
|||
return table, errs[0]
|
||||
}
|
||||
|
||||
nevts, errs := v.namespacedPolicies()
|
||||
nevts, errs := p.namespacedPolicies()
|
||||
if len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
log.Error().Err(err).Msg("Unable to find cluster policies")
|
||||
|
|
@ -147,28 +157,28 @@ func (v *policyView) reconcile() (resource.TableData, error) {
|
|||
evts[k] = v
|
||||
}
|
||||
|
||||
return buildTable(v, evts), nil
|
||||
return buildTable(p, evts), nil
|
||||
}
|
||||
|
||||
// Protocol...
|
||||
|
||||
func (v *policyView) header() resource.Row {
|
||||
func (p *Policy) header() resource.Row {
|
||||
return policyHeader
|
||||
}
|
||||
|
||||
func (v *policyView) getCache() resource.RowEvents {
|
||||
return v.cache
|
||||
func (p *Policy) getCache() resource.RowEvents {
|
||||
return p.cache
|
||||
}
|
||||
|
||||
func (v *policyView) setCache(evts resource.RowEvents) {
|
||||
v.cache = evts
|
||||
func (p *Policy) setCache(evts resource.RowEvents) {
|
||||
p.cache = evts
|
||||
}
|
||||
|
||||
func (v *policyView) clusterPolicies() (resource.RowEvents, []error) {
|
||||
func (p *Policy) clusterPolicies() (resource.RowEvents, []error) {
|
||||
var errs []error
|
||||
evts := make(resource.RowEvents)
|
||||
|
||||
crbs, err := v.app.Conn().DialOrDie().RbacV1().ClusterRoleBindings().List(metav1.ListOptions{})
|
||||
crbs, err := p.app.Conn().DialOrDie().RbacV1().ClusterRoleBindings().List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return evts, errs
|
||||
}
|
||||
|
|
@ -176,18 +186,18 @@ func (v *policyView) clusterPolicies() (resource.RowEvents, []error) {
|
|||
var rr []string
|
||||
for _, crb := range crbs.Items {
|
||||
for _, s := range crb.Subjects {
|
||||
if s.Kind == v.subjectKind && s.Name == v.subjectName {
|
||||
if s.Kind == p.subjectKind && s.Name == p.subjectName {
|
||||
rr = append(rr, crb.RoleRef.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range rr {
|
||||
role, err := v.app.Conn().DialOrDie().RbacV1().ClusterRoles().Get(r, metav1.GetOptions{})
|
||||
role, err := p.app.Conn().DialOrDie().RbacV1().ClusterRoles().Get(r, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
for k, v := range v.parseRules("*", "CR:"+r, role.Rules) {
|
||||
for k, v := range p.parseRules("*", "CR:"+r, role.Rules) {
|
||||
evts[k] = v
|
||||
}
|
||||
}
|
||||
|
|
@ -195,10 +205,10 @@ func (v *policyView) clusterPolicies() (resource.RowEvents, []error) {
|
|||
return evts, errs
|
||||
}
|
||||
|
||||
func (v policyView) loadRoleBindings() ([]namespacedRole, error) {
|
||||
func (p *Policy) loadRoleBindings() ([]namespacedRole, error) {
|
||||
var rr []namespacedRole
|
||||
|
||||
dial := v.app.Conn().DialOrDie().RbacV1()
|
||||
dial := p.app.Conn().DialOrDie().RbacV1()
|
||||
rbs, err := dial.RoleBindings("").List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return rr, err
|
||||
|
|
@ -206,7 +216,7 @@ func (v policyView) loadRoleBindings() ([]namespacedRole, error) {
|
|||
|
||||
for _, rb := range rbs.Items {
|
||||
for _, s := range rb.Subjects {
|
||||
if s.Kind == v.subjectKind && s.Name == v.subjectName {
|
||||
if s.Kind == p.subjectKind && s.Name == p.subjectName {
|
||||
rr = append(rr, namespacedRole{rb.Namespace, rb.RoleRef.Name})
|
||||
}
|
||||
}
|
||||
|
|
@ -215,16 +225,16 @@ func (v policyView) loadRoleBindings() ([]namespacedRole, error) {
|
|||
return rr, nil
|
||||
}
|
||||
|
||||
func (v *policyView) loadRoles(errs []error, rr []namespacedRole) (resource.RowEvents, []error) {
|
||||
func (p *Policy) loadRoles(errs []error, rr []namespacedRole) (resource.RowEvents, []error) {
|
||||
var (
|
||||
dial = v.app.Conn().DialOrDie().RbacV1()
|
||||
dial = p.app.Conn().DialOrDie().RbacV1()
|
||||
evts = make(resource.RowEvents)
|
||||
)
|
||||
for _, r := range rr {
|
||||
if cr, err := dial.Roles(r.ns).Get(r.role, metav1.GetOptions{}); err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
for k, v := range v.parseRules(r.ns, "RO:"+r.role, cr.Rules) {
|
||||
for k, v := range p.parseRules(r.ns, "RO:"+r.role, cr.Rules) {
|
||||
evts[k] = v
|
||||
}
|
||||
}
|
||||
|
|
@ -233,18 +243,18 @@ func (v *policyView) loadRoles(errs []error, rr []namespacedRole) (resource.RowE
|
|||
return evts, errs
|
||||
}
|
||||
|
||||
func (v *policyView) namespacedPolicies() (resource.RowEvents, []error) {
|
||||
func (p *Policy) namespacedPolicies() (resource.RowEvents, []error) {
|
||||
var errs []error
|
||||
rr, err := v.loadRoleBindings()
|
||||
rr, err := p.loadRoleBindings()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
evts, errs := v.loadRoles(errs, rr)
|
||||
evts, errs := p.loadRoles(errs, rr)
|
||||
return evts, errs
|
||||
}
|
||||
|
||||
func (v *policyView) parseRules(ns, binding string, rules []rbacv1.PolicyRule) resource.RowEvents {
|
||||
func (p *Policy) parseRules(ns, binding string, rules []rbacv1.PolicyRule) resource.RowEvents {
|
||||
m := make(resource.RowEvents, len(rules))
|
||||
for _, r := range rules {
|
||||
for _, grp := range r.APIGroups {
|
||||
|
|
@ -0,0 +1,389 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/perf"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
forwardTitle = "Port Forwards"
|
||||
forwardTitleFmt = " [aqua::b]%s([fuchsia::b]%d[fuchsia::-])[aqua::-] "
|
||||
promptPage = "prompt"
|
||||
)
|
||||
|
||||
// PortForward presents active portforward viewer.
|
||||
type PortForward struct {
|
||||
*ui.Pages
|
||||
|
||||
cancelFn context.CancelFunc
|
||||
bench *perf.Benchmark
|
||||
app *App
|
||||
}
|
||||
|
||||
// NewPortForward returns a new viewer.
|
||||
func NewPortForward(title, _ string, list resource.List) ResourceViewer {
|
||||
return &PortForward{
|
||||
Pages: ui.NewPages(),
|
||||
}
|
||||
}
|
||||
|
||||
// Init the view.
|
||||
func (p *PortForward) Init(ctx context.Context) {
|
||||
p.app = ctx.Value(ui.KeyApp).(*App)
|
||||
|
||||
tv := NewTable(forwardTitle)
|
||||
tv.Init(ctx)
|
||||
tv.SetBorderFocusColor(tcell.ColorDodgerBlue)
|
||||
tv.SetSelectedStyle(tcell.ColorWhite, tcell.ColorDodgerBlue, tcell.AttrNone)
|
||||
tv.SetColorerFn(forwardColorer)
|
||||
tv.SetActiveNS("")
|
||||
tv.SetSortCol(tv.NameColIndex()+6, 0, true)
|
||||
tv.Select(1, 0)
|
||||
p.Push(tv)
|
||||
|
||||
p.registerActions()
|
||||
p.Start()
|
||||
p.refresh()
|
||||
}
|
||||
|
||||
func (p *PortForward) Start() {
|
||||
path := ui.BenchConfig(p.app.Config.K9s.CurrentCluster)
|
||||
var ctx context.Context
|
||||
ctx, p.cancelFn = context.WithCancel(context.Background())
|
||||
if err := watchFS(ctx, p.app, config.K9sHome, path, p.reload); err != nil {
|
||||
p.app.Flash().Errf("RuRoh! Unable to watch benchmarks directory %s : %s", config.K9sHome, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PortForward) Stop() {}
|
||||
|
||||
func (p *PortForward) Name() string {
|
||||
return "portForwards"
|
||||
}
|
||||
|
||||
func (p *PortForward) masterPage() *Table {
|
||||
return p.GetPrimitive("table").(*Table)
|
||||
}
|
||||
|
||||
func (p *PortForward) setEnterFn(enterFn) {}
|
||||
func (p *PortForward) setColorerFn(ui.ColorerFunc) {}
|
||||
func (p *PortForward) setDecorateFn(decorateFn) {}
|
||||
func (p *PortForward) setExtraActionsFn(ActionsFunc) {}
|
||||
|
||||
func (p *PortForward) getTV() *Table {
|
||||
if vu, ok := p.GetPrimitive("table").(*Table); ok {
|
||||
return vu
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PortForward) reload() {
|
||||
path := ui.BenchConfig(p.app.Config.K9s.CurrentCluster)
|
||||
log.Debug().Msgf("Reloading Config %s", path)
|
||||
if err := p.app.Bench.Reload(path); err != nil {
|
||||
p.app.Flash().Err(err)
|
||||
}
|
||||
p.refresh()
|
||||
}
|
||||
|
||||
func (p *PortForward) refresh() {
|
||||
tv := p.getTV()
|
||||
tv.Update(p.hydrate())
|
||||
p.app.SetFocus(tv)
|
||||
tv.UpdateTitle()
|
||||
}
|
||||
|
||||
func (p *PortForward) registerActions() {
|
||||
tv := p.getTV()
|
||||
tv.AddActions(ui.KeyActions{
|
||||
tcell.KeyEnter: ui.NewKeyAction("Goto", p.gotoBenchCmd, true),
|
||||
tcell.KeyCtrlB: ui.NewKeyAction("Bench", p.benchCmd, true),
|
||||
tcell.KeyCtrlK: ui.NewKeyAction("Bench Stop", p.benchStopCmd, true),
|
||||
tcell.KeyCtrlD: ui.NewKeyAction("Delete", p.deleteCmd, true),
|
||||
ui.KeySlash: ui.NewKeyAction("Filter", tv.activateCmd, false),
|
||||
ui.KeyP: ui.NewKeyAction("Previous", p.app.PrevCmd, false),
|
||||
ui.KeyShiftP: ui.NewKeyAction("Sort Ports", p.sortColCmd(2, true), false),
|
||||
ui.KeyShiftU: ui.NewKeyAction("Sort URL", p.sortColCmd(4, true), false),
|
||||
})
|
||||
}
|
||||
|
||||
func (p *PortForward) getTitle() string {
|
||||
return forwardTitle
|
||||
}
|
||||
|
||||
func (p *PortForward) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
tv := p.getTV()
|
||||
tv.SetSortCol(tv.NameColIndex()+col, 0, asc)
|
||||
p.refresh()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PortForward) gotoBenchCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
p.app.gotoResource("be", true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PortForward) benchStopCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if p.bench != nil {
|
||||
log.Debug().Msg(">>> Benchmark cancelFned!!")
|
||||
p.app.status(ui.FlashErr, "Benchmark Camceled!")
|
||||
p.bench.Cancel()
|
||||
}
|
||||
p.app.StatusReset()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PortForward) benchCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
sel := p.getSelectedItem()
|
||||
if sel == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.bench != nil {
|
||||
p.app.Flash().Err(errors.New("Only one benchmark allowed at a time"))
|
||||
return nil
|
||||
}
|
||||
|
||||
tv := p.getTV()
|
||||
r, _ := tv.GetSelection()
|
||||
cfg, co := defaultConfig(), ui.TrimCell(tv.Table, r, 2)
|
||||
if b, ok := p.app.Bench.Benchmarks.Containers[containerID(sel, co)]; ok {
|
||||
cfg = b
|
||||
}
|
||||
cfg.Name = sel
|
||||
|
||||
base := ui.TrimCell(tv.Table, r, 4)
|
||||
var err error
|
||||
if p.bench, err = perf.NewBenchmark(base, cfg); err != nil {
|
||||
p.app.Flash().Errf("Bench failed %v", err)
|
||||
p.app.StatusReset()
|
||||
return nil
|
||||
}
|
||||
|
||||
p.app.status(ui.FlashWarn, "Benchmark in progress...")
|
||||
log.Debug().Msg("Bench starting...")
|
||||
go p.runBenchmark()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PortForward) runBenchmark() {
|
||||
p.bench.Run(p.app.Config.K9s.CurrentCluster, func() {
|
||||
log.Debug().Msg("Bench Completed!")
|
||||
p.app.QueueUpdate(func() {
|
||||
if p.bench.Canceled() {
|
||||
p.app.status(ui.FlashInfo, "Benchmark cancelFned")
|
||||
} else {
|
||||
p.app.status(ui.FlashInfo, "Benchmark Completed!")
|
||||
p.bench.Cancel()
|
||||
}
|
||||
p.bench = nil
|
||||
go func() {
|
||||
<-time.After(2 * time.Second)
|
||||
p.app.QueueUpdate(func() { p.app.StatusReset() })
|
||||
}()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (p *PortForward) getSelectedItem() string {
|
||||
tv := p.getTV()
|
||||
r, _ := tv.GetSelection()
|
||||
if r == 0 {
|
||||
return ""
|
||||
}
|
||||
return fwFQN(
|
||||
fqn(ui.TrimCell(tv.Table, r, 0), ui.TrimCell(tv.Table, r, 1)),
|
||||
ui.TrimCell(tv.Table, r, 2),
|
||||
)
|
||||
}
|
||||
|
||||
func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
tv := p.getTV()
|
||||
if !tv.SearchBuff().Empty() {
|
||||
tv.SearchBuff().Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
sel := p.getSelectedItem()
|
||||
if sel == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
showModal(p.Pages, fmt.Sprintf("Delete PortForward `%s?", sel), "table", func() {
|
||||
fw, ok := p.app.forwarders[sel]
|
||||
if !ok {
|
||||
log.Debug().Msgf("Unable to find forwarder %s", sel)
|
||||
return
|
||||
}
|
||||
fw.Stop()
|
||||
delete(p.app.forwarders, sel)
|
||||
|
||||
log.Debug().Msgf("PortForwards after delete: %#v", p.app.forwarders)
|
||||
p.getTV().Update(p.hydrate())
|
||||
p.app.Flash().Infof("PortForward %s deleted!", sel)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PortForward) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if p.cancelFn != nil {
|
||||
p.cancelFn()
|
||||
}
|
||||
|
||||
tv := p.getTV()
|
||||
if tv.SearchBuff().IsActive() {
|
||||
tv.SearchBuff().Reset()
|
||||
} else {
|
||||
p.app.inject(p.app.Content.GetPrimitive("main").(model.Component))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PortForward) Hints() model.MenuHints {
|
||||
return p.getTV().Hints()
|
||||
}
|
||||
|
||||
func (p *PortForward) hydrate() resource.TableData {
|
||||
data := initHeader(len(p.app.forwarders))
|
||||
dc, dn := p.app.Bench.Benchmarks.Defaults.C, p.app.Bench.Benchmarks.Defaults.N
|
||||
for _, f := range p.app.forwarders {
|
||||
c, n, cfg := loadConfig(dc, dn, containerID(f.Path(), f.Container()), p.app.Bench.Benchmarks.Containers)
|
||||
|
||||
ports := strings.Split(f.Ports()[0], ":")
|
||||
ns, na := namespaced(f.Path())
|
||||
fields := resource.Row{
|
||||
ns,
|
||||
na,
|
||||
f.Container(),
|
||||
strings.Join(f.Ports(), ","),
|
||||
urlFor(cfg, f.Container(), ports[0]),
|
||||
asNum(c),
|
||||
asNum(n),
|
||||
f.Age(),
|
||||
}
|
||||
data.Rows[f.Path()] = &resource.RowEvent{
|
||||
Action: resource.New,
|
||||
Fields: fields,
|
||||
Deltas: fields,
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func (p *PortForward) resetTitle() {
|
||||
p.SetTitle(fmt.Sprintf(forwardTitleFmt, forwardTitle, p.getTV().GetRowCount()-1))
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func defaultConfig() config.BenchConfig {
|
||||
return config.BenchConfig{
|
||||
C: config.DefaultC,
|
||||
N: config.DefaultN,
|
||||
HTTP: config.HTTP{
|
||||
Method: config.DefaultMethod,
|
||||
Path: "/",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func initHeader(rows int) resource.TableData {
|
||||
return resource.TableData{
|
||||
Header: resource.Row{"NAMESPACE", "NAME", "CONTAINER", "PORTS", "URL", "C", "N", "AGE"},
|
||||
NumCols: map[string]bool{"C": true, "N": true},
|
||||
Rows: make(resource.RowEvents, rows),
|
||||
Namespace: resource.AllNamespaces,
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfig(dc, dn int, id string, cc map[string]config.BenchConfig) (int, int, config.BenchConfig) {
|
||||
c, n := dc, dn
|
||||
cfg, ok := cc[id]
|
||||
if !ok {
|
||||
return c, n, cfg
|
||||
}
|
||||
|
||||
if cfg.C != 0 {
|
||||
c = cfg.C
|
||||
}
|
||||
if cfg.N != 0 {
|
||||
n = cfg.N
|
||||
}
|
||||
|
||||
return c, n, cfg
|
||||
}
|
||||
|
||||
func showModal(p *ui.Pages, msg, back string, ok func()) {
|
||||
m := tview.NewModal().
|
||||
AddButtons([]string{"Cancel", "OK"}).
|
||||
SetTextColor(tcell.ColorFuchsia).
|
||||
SetText(msg).
|
||||
SetDoneFunc(func(_ int, b string) {
|
||||
if b == "OK" {
|
||||
ok()
|
||||
}
|
||||
dismissModal(p, back)
|
||||
})
|
||||
m.SetTitle("<Delete Benchmark>")
|
||||
p.AddPage(promptPage, m, false, false)
|
||||
p.ShowPage(promptPage)
|
||||
}
|
||||
|
||||
func dismissModal(p *ui.Pages, page string) {
|
||||
p.RemovePage(promptPage)
|
||||
p.SwitchToPage(page)
|
||||
}
|
||||
|
||||
func watchFS(ctx context.Context, app *App, dir, file string, cb func()) error {
|
||||
w, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case evt := <-w.Events:
|
||||
log.Debug().Msgf("FS %s event %v", file, evt.Name)
|
||||
if file == "" || evt.Name == file {
|
||||
log.Debug().Msgf("Capuring Event %#v", evt)
|
||||
app.QueueUpdateDraw(func() {
|
||||
cb()
|
||||
})
|
||||
}
|
||||
case err := <-w.Errors:
|
||||
log.Info().Err(err).Msgf("FS %s watcher failed", dir)
|
||||
return
|
||||
case <-ctx.Done():
|
||||
log.Debug().Msgf("<<FS %s WATCHER DONE>>", dir)
|
||||
w.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return w.Add(dir)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package views
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/derailed/tview"
|
||||
|
|
@ -19,7 +19,7 @@ func newSelector(title, port string, okFn, cancelFn func()) *portSelector {
|
|||
}
|
||||
}
|
||||
|
||||
func (p *portSelector) show(app *appView) {
|
||||
func (p *portSelector) show(app *App) {
|
||||
f := tview.NewForm()
|
||||
f.SetItemPadding(0)
|
||||
f.SetButtonsAlign(tview.AlignCenter).
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package views
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -64,132 +64,146 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
type (
|
||||
roleKind = int8
|
||||
type roleKind = int8
|
||||
|
||||
rbacView struct {
|
||||
*tableView
|
||||
// RBAC presents an RBAC policy viewer.
|
||||
type RBAC struct {
|
||||
*Table
|
||||
|
||||
app *appView
|
||||
current ui.Igniter
|
||||
cancel context.CancelFunc
|
||||
roleType roleKind
|
||||
roleName string
|
||||
cache resource.RowEvents
|
||||
}
|
||||
)
|
||||
app *App
|
||||
cancelFn context.CancelFunc
|
||||
roleType roleKind
|
||||
roleName string
|
||||
cache resource.RowEvents
|
||||
}
|
||||
|
||||
func newRBACView(app *appView, ns, name string, kind roleKind) *rbacView {
|
||||
v := rbacView{
|
||||
// NewRBAC returns a new viewer.
|
||||
func NewRBAC(app *App, ns, name string, kind roleKind) *RBAC {
|
||||
r := RBAC{
|
||||
app: app,
|
||||
roleName: name,
|
||||
roleType: kind,
|
||||
}
|
||||
v.tableView = newTableView(app, v.getTitle())
|
||||
v.SetActiveNS(ns)
|
||||
v.SetColorerFn(rbacColorer)
|
||||
v.current = app.Frame().GetPrimitive("main").(ui.Igniter)
|
||||
v.bindKeys()
|
||||
r.Table = NewTable(r.getTitle())
|
||||
r.SetActiveNS(ns)
|
||||
r.SetColorerFn(rbacColorer)
|
||||
r.bindKeys()
|
||||
|
||||
return &v
|
||||
return &r
|
||||
}
|
||||
|
||||
// Init the view.
|
||||
func (v *rbacView) Init(c context.Context, ns string) {
|
||||
v.SetSortCol(1, len(rbacHeader), true)
|
||||
// Init initializes the view.
|
||||
func (r *RBAC) Init(ctx context.Context) {
|
||||
r.Table.Init(ctx)
|
||||
|
||||
r.Start()
|
||||
r.SetSortCol(1, len(rbacHeader), true)
|
||||
r.refresh()
|
||||
}
|
||||
|
||||
// Start watches for viewer updates
|
||||
func (r *RBAC) Start() {
|
||||
r.Stop()
|
||||
|
||||
var ctx context.Context
|
||||
ctx, r.cancelFn = context.WithCancel(context.Background())
|
||||
|
||||
ctx, cancel := context.WithCancel(c)
|
||||
v.cancel = cancel
|
||||
go func(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(time.Duration(v.app.Config.K9s.GetRefreshRate()) * time.Second):
|
||||
v.app.QueueUpdateDraw(func() {
|
||||
v.refresh()
|
||||
case <-time.After(time.Duration(r.app.Config.K9s.GetRefreshRate()) * time.Second):
|
||||
r.app.QueueUpdateDraw(func() {
|
||||
r.refresh()
|
||||
})
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
|
||||
v.refresh()
|
||||
v.app.SetHints(v.Hints())
|
||||
v.app.SetFocus(v)
|
||||
}
|
||||
|
||||
func (v *rbacView) bindKeys() {
|
||||
v.RmAction(ui.KeyShiftA)
|
||||
// Stop terminates the viewer updater.
|
||||
func (r *RBAC) Stop() {
|
||||
if r.cancelFn != nil {
|
||||
r.cancelFn()
|
||||
}
|
||||
}
|
||||
|
||||
v.SetActions(ui.KeyActions{
|
||||
tcell.KeyEscape: ui.NewKeyAction("Reset", v.resetCmd, false),
|
||||
ui.KeySlash: ui.NewKeyAction("Filter", v.activateCmd, false),
|
||||
ui.KeyP: ui.NewKeyAction("Previous", v.app.prevCmd, false),
|
||||
ui.KeyShiftO: ui.NewKeyAction("Sort APIGroup", v.SortColCmd(1), false),
|
||||
// Name returns the component name.
|
||||
func (r *RBAC) Name() string {
|
||||
return rbacTitle
|
||||
}
|
||||
|
||||
func (r *RBAC) bindKeys() {
|
||||
r.RmAction(ui.KeyShiftA)
|
||||
|
||||
r.AddActions(ui.KeyActions{
|
||||
tcell.KeyEscape: ui.NewKeyAction("Reset", r.resetCmd, false),
|
||||
ui.KeySlash: ui.NewKeyAction("Filter", r.activateCmd, false),
|
||||
ui.KeyP: ui.NewKeyAction("Previous", r.app.PrevCmd, false),
|
||||
ui.KeyShiftO: ui.NewKeyAction("Sort APIGroup", r.SortColCmd(1), false),
|
||||
})
|
||||
}
|
||||
|
||||
func (v *rbacView) getTitle() string {
|
||||
return skinTitle(fmt.Sprintf(rbacTitleFmt, rbacTitle, v.roleName), v.app.Styles.Frame())
|
||||
func (r *RBAC) getTitle() string {
|
||||
return skinTitle(fmt.Sprintf(rbacTitleFmt, rbacTitle, r.roleName), r.app.Styles.Frame())
|
||||
}
|
||||
|
||||
func (v *rbacView) refresh() {
|
||||
data, err := v.reconcile(v.ActiveNS(), v.roleName, v.roleType)
|
||||
func (r *RBAC) refresh() {
|
||||
data, err := r.reconcile(r.ActiveNS(), r.roleName, r.roleType)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Refresh for %s:%d", v.roleName, v.roleType)
|
||||
v.app.Flash().Err(err)
|
||||
log.Error().Err(err).Msgf("Refresh for %s:%d", r.roleName, r.roleType)
|
||||
r.app.Flash().Err(err)
|
||||
}
|
||||
v.Update(data)
|
||||
r.Update(data)
|
||||
}
|
||||
|
||||
func (v *rbacView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !v.SearchBuff().Empty() {
|
||||
v.SearchBuff().Reset()
|
||||
func (r *RBAC) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !r.SearchBuff().Empty() {
|
||||
r.SearchBuff().Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
return v.backCmd(evt)
|
||||
return r.backCmd(evt)
|
||||
}
|
||||
|
||||
func (v *rbacView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if v.cancel != nil {
|
||||
v.cancel()
|
||||
func (r *RBAC) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if r.cancelFn != nil {
|
||||
r.cancelFn()
|
||||
}
|
||||
|
||||
if v.SearchBuff().IsActive() {
|
||||
v.SearchBuff().Reset()
|
||||
if r.SearchBuff().IsActive() {
|
||||
r.SearchBuff().Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
v.app.inject(v.current)
|
||||
|
||||
return nil
|
||||
return r.app.PrevCmd(evt)
|
||||
}
|
||||
|
||||
func (v *rbacView) reconcile(ns, name string, kind roleKind) (resource.TableData, error) {
|
||||
func (r *RBAC) reconcile(ns, name string, kind roleKind) (resource.TableData, error) {
|
||||
var table resource.TableData
|
||||
|
||||
evts, err := v.rowEvents(ns, name, kind)
|
||||
evts, err := r.rowEvents(ns, name, kind)
|
||||
if err != nil {
|
||||
return table, err
|
||||
}
|
||||
|
||||
return buildTable(v, evts), nil
|
||||
return buildTable(r, evts), nil
|
||||
}
|
||||
|
||||
func (v *rbacView) header() resource.Row {
|
||||
func (r *RBAC) header() resource.Row {
|
||||
return rbacHeader
|
||||
}
|
||||
|
||||
func (v *rbacView) getCache() resource.RowEvents {
|
||||
return v.cache
|
||||
func (r *RBAC) getCache() resource.RowEvents {
|
||||
return r.cache
|
||||
}
|
||||
|
||||
func (v *rbacView) setCache(evts resource.RowEvents) {
|
||||
v.cache = evts
|
||||
func (r *RBAC) setCache(evts resource.RowEvents) {
|
||||
r.cache = evts
|
||||
}
|
||||
|
||||
func (v *rbacView) rowEvents(ns, name string, kind roleKind) (resource.RowEvents, error) {
|
||||
func (r *RBAC) rowEvents(ns, name string, kind roleKind) (resource.RowEvents, error) {
|
||||
var (
|
||||
evts resource.RowEvents
|
||||
err error
|
||||
|
|
@ -197,9 +211,9 @@ func (v *rbacView) rowEvents(ns, name string, kind roleKind) (resource.RowEvents
|
|||
|
||||
switch kind {
|
||||
case clusterRole:
|
||||
evts, err = v.clusterPolicies(name)
|
||||
evts, err = r.clusterPolicies(name)
|
||||
case role:
|
||||
evts, err = v.namespacedPolicies(name)
|
||||
evts, err = r.namespacedPolicies(name)
|
||||
default:
|
||||
return evts, fmt.Errorf("Expecting clusterrole/role but found %d", kind)
|
||||
}
|
||||
|
|
@ -211,26 +225,26 @@ func (v *rbacView) rowEvents(ns, name string, kind roleKind) (resource.RowEvents
|
|||
return evts, nil
|
||||
}
|
||||
|
||||
func (v *rbacView) clusterPolicies(name string) (resource.RowEvents, error) {
|
||||
cr, err := v.app.Conn().DialOrDie().RbacV1().ClusterRoles().Get(name, metav1.GetOptions{})
|
||||
func (r *RBAC) clusterPolicies(name string) (resource.RowEvents, error) {
|
||||
cr, err := r.app.Conn().DialOrDie().RbacV1().ClusterRoles().Get(name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v.parseRules(cr.Rules), nil
|
||||
return r.parseRules(cr.Rules), nil
|
||||
}
|
||||
|
||||
func (v *rbacView) namespacedPolicies(path string) (resource.RowEvents, error) {
|
||||
func (r *RBAC) namespacedPolicies(path string) (resource.RowEvents, error) {
|
||||
ns, na := namespaced(path)
|
||||
cr, err := v.app.Conn().DialOrDie().RbacV1().Roles(ns).Get(na, metav1.GetOptions{})
|
||||
cr, err := r.app.Conn().DialOrDie().RbacV1().Roles(ns).Get(na, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v.parseRules(cr.Rules), nil
|
||||
return r.parseRules(cr.Rules), nil
|
||||
}
|
||||
|
||||
func (v *rbacView) parseRules(rules []rbacv1.PolicyRule) resource.RowEvents {
|
||||
func (r *RBAC) parseRules(rules []rbacv1.PolicyRule) resource.RowEvents {
|
||||
m := make(resource.RowEvents, len(rules))
|
||||
for _, r := range rules {
|
||||
for _, grp := range r.APIGroups {
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
package view
|
||||
|
||||
// import (
|
||||
// "testing"
|
||||
|
||||
// "github.com/derailed/k9s/internal/resource"
|
||||
// "github.com/stretchr/testify/assert"
|
||||
// rbacv1 "k8s.io/api/rbac/v1"
|
||||
// )
|
||||
|
||||
// func TestHasVerb(t *testing.T) {
|
||||
// uu := []struct {
|
||||
// vv []string
|
||||
// v string
|
||||
// e bool
|
||||
// }{
|
||||
// {[]string{"*"}, "get", true},
|
||||
// {[]string{"get", "list", "watch"}, "watch", true},
|
||||
// {[]string{"get", "dope", "list"}, "watch", false},
|
||||
// {[]string{"get"}, "get", true},
|
||||
// {[]string{"post"}, "create", true},
|
||||
// {[]string{"put"}, "update", true},
|
||||
// {[]string{"list", "deletecollection"}, "deletecollection", true},
|
||||
// }
|
||||
|
||||
// for _, u := range uu {
|
||||
// assert.Equal(t, u.e, hasVerb(u.vv, u.v))
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestAsVerbs(t *testing.T) {
|
||||
// ok, nok := toVerbIcon(true), toVerbIcon(false)
|
||||
|
||||
// uu := []struct {
|
||||
// vv []string
|
||||
// e resource.Row
|
||||
// }{
|
||||
// {[]string{"*"}, resource.Row{ok, ok, ok, ok, ok, ok, ok, ok, ""}},
|
||||
// {[]string{"get", "list", "patch"}, resource.Row{ok, ok, nok, nok, nok, ok, nok, nok, ""}},
|
||||
// {[]string{"get", "list", "deletecollection", "post"}, resource.Row{ok, ok, ok, nok, ok, nok, nok, nok, ""}},
|
||||
// {[]string{"get", "list", "blee"}, resource.Row{ok, ok, nok, nok, nok, nok, nok, nok, "blee"}},
|
||||
// }
|
||||
|
||||
// for _, u := range uu {
|
||||
// assert.Equal(t, u.e, asVerbs(u.vv...))
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestParseRules(t *testing.T) {
|
||||
// ok, nok := toVerbIcon(true), toVerbIcon(false)
|
||||
// _ = nok
|
||||
|
||||
// uu := []struct {
|
||||
// pp []rbacv1.PolicyRule
|
||||
// e map[string]resource.Row
|
||||
// }{
|
||||
// {
|
||||
// []rbacv1.PolicyRule{
|
||||
// {APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"*"}},
|
||||
// },
|
||||
// map[string]resource.Row{
|
||||
// "*.*": {"*.*", "*", ok, ok, ok, ok, ok, ok, ok, ok, ""},
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// []rbacv1.PolicyRule{
|
||||
// {APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"get"}},
|
||||
// },
|
||||
// map[string]resource.Row{
|
||||
// "*.*": {"*.*", "*", ok, nok, nok, nok, nok, nok, nok, nok, ""},
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// []rbacv1.PolicyRule{
|
||||
// {APIGroups: []string{""}, Resources: []string{"*"}, Verbs: []string{"list"}},
|
||||
// },
|
||||
// map[string]resource.Row{
|
||||
// "*": {"*", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""},
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// []rbacv1.PolicyRule{
|
||||
// {APIGroups: []string{""}, Resources: []string{"pods"}, Verbs: []string{"list"}, ResourceNames: []string{"fred"}},
|
||||
// },
|
||||
// map[string]resource.Row{
|
||||
// "pods": {"pods", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""},
|
||||
// "pods/fred": {"pods/fred", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""},
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// []rbacv1.PolicyRule{
|
||||
// {APIGroups: []string{}, Resources: []string{}, Verbs: []string{"get"}, NonResourceURLs: []string{"/fred"}},
|
||||
// },
|
||||
// map[string]resource.Row{
|
||||
// "/fred": {"/fred", resource.NAValue, ok, nok, nok, nok, nok, nok, nok, nok, ""},
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// []rbacv1.PolicyRule{
|
||||
// {APIGroups: []string{}, Resources: []string{}, Verbs: []string{"get"}, NonResourceURLs: []string{"fred"}},
|
||||
// },
|
||||
// map[string]resource.Row{
|
||||
// "/fred": {"/fred", resource.NAValue, ok, nok, nok, nok, nok, nok, nok, nok, ""},
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
|
||||
// var v rbacView
|
||||
// for _, u := range uu {
|
||||
// evts := v.parseRules(u.pp)
|
||||
// for k, v := range u.e {
|
||||
// assert.Equal(t, v, evts[k].Fields)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
package views
|
||||
package view
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
|
|
@ -13,9 +12,9 @@ import (
|
|||
)
|
||||
|
||||
type (
|
||||
viewFn func(title, gvr string, app *appView, list resource.List) resourceViewer
|
||||
viewFn func(title, gvr string, list resource.List) ResourceViewer
|
||||
listFn func(c resource.Connection, ns string) resource.List
|
||||
enterFn func(app *appView, ns, resource, selection string)
|
||||
enterFn func(app *App, ns, resource, selection string)
|
||||
decorateFn func(resource.TableData) resource.TableData
|
||||
|
||||
viewer struct {
|
||||
|
|
@ -34,8 +33,8 @@ type (
|
|||
)
|
||||
|
||||
func listFunc(l resource.List) viewFn {
|
||||
return func(title, gvr string, app *appView, list resource.List) resourceViewer {
|
||||
return newResourceView(title, gvr, app, l)
|
||||
return func(title, gvr string, list resource.List) ResourceViewer {
|
||||
return NewResource(title, gvr, l)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +49,6 @@ func allCRDs(c k8s.Connection, vv viewers) {
|
|||
return
|
||||
}
|
||||
|
||||
t := time.Now()
|
||||
for _, crd := range crds {
|
||||
meta, err := crd.ExtFields()
|
||||
if err != nil {
|
||||
|
|
@ -77,46 +75,45 @@ func allCRDs(c k8s.Connection, vv viewers) {
|
|||
colorerFn: ui.DefaultColorer,
|
||||
}
|
||||
}
|
||||
log.Debug().Msgf("Loading CRDS %v", time.Since(t))
|
||||
}
|
||||
|
||||
func showRBAC(app *appView, ns, resource, selection string) {
|
||||
func showRBAC(app *App, ns, resource, selection string) {
|
||||
kind := clusterRole
|
||||
if resource == "role" {
|
||||
kind = role
|
||||
}
|
||||
app.inject(newRBACView(app, ns, selection, kind))
|
||||
app.inject(NewRBAC(app, ns, selection, kind))
|
||||
}
|
||||
|
||||
func showCRD(app *appView, ns, resource, selection string) {
|
||||
func showCRD(app *App, ns, resource, selection string) {
|
||||
log.Debug().Msgf("Launching CRD %q -- %q -- %q", ns, resource, selection)
|
||||
tokens := strings.Split(selection, ".")
|
||||
app.gotoResource(tokens[0], true)
|
||||
|
||||
}
|
||||
|
||||
func showClusterRole(app *appView, ns, resource, selection string) {
|
||||
func showClusterRole(app *App, ns, resource, selection string) {
|
||||
crb, err := app.Conn().DialOrDie().RbacV1().ClusterRoleBindings().Get(selection, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
app.Flash().Errf("Unable to retrieve clusterrolebindings for %s", selection)
|
||||
return
|
||||
}
|
||||
app.inject(newRBACView(app, ns, crb.RoleRef.Name, clusterRole))
|
||||
app.inject(NewRBAC(app, ns, crb.RoleRef.Name, clusterRole))
|
||||
}
|
||||
|
||||
func showRole(app *appView, _, resource, selection string) {
|
||||
func showRole(app *App, _, resource, selection string) {
|
||||
ns, n := namespaced(selection)
|
||||
rb, err := app.Conn().DialOrDie().RbacV1().RoleBindings(ns).Get(n, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
app.Flash().Errf("Unable to retrieve rolebindings for %s", selection)
|
||||
return
|
||||
}
|
||||
app.inject(newRBACView(app, ns, fqn(ns, rb.RoleRef.Name), role))
|
||||
app.inject(NewRBAC(app, ns, fqn(ns, rb.RoleRef.Name), role))
|
||||
}
|
||||
|
||||
func showSAPolicy(app *appView, _, _, selection string) {
|
||||
func showSAPolicy(app *App, _, _, selection string) {
|
||||
_, n := namespaced(selection)
|
||||
app.inject(newPolicyView(app, mapFuSubject("ServiceAccount"), n))
|
||||
app.inject(NewPolicy(app, mapFuSubject("ServiceAccount"), n))
|
||||
}
|
||||
|
||||
func load(c k8s.Connection, vv viewers) {
|
||||
|
|
@ -157,10 +154,6 @@ func load(c k8s.Connection, vv viewers) {
|
|||
}
|
||||
|
||||
func resourceViews(c k8s.Connection, m viewers) {
|
||||
defer func(t time.Time) {
|
||||
log.Debug().Msgf("Loading Views Elapsed %v", time.Since(t))
|
||||
}(time.Now())
|
||||
|
||||
coreRes(m)
|
||||
miscRes(m)
|
||||
appsRes(m)
|
||||
|
|
@ -176,17 +169,17 @@ func resourceViews(c k8s.Connection, m viewers) {
|
|||
|
||||
func coreRes(vv viewers) {
|
||||
vv["v1/nodes"] = viewer{
|
||||
viewFn: newNodeView,
|
||||
viewFn: NewNode,
|
||||
listFn: resource.NewNodeList,
|
||||
colorerFn: nsColorer,
|
||||
}
|
||||
vv["v1/namespaces"] = viewer{
|
||||
viewFn: newNamespaceView,
|
||||
viewFn: NewNamespace,
|
||||
listFn: resource.NewNamespaceList,
|
||||
colorerFn: nsColorer,
|
||||
}
|
||||
vv["v1/pods"] = viewer{
|
||||
viewFn: newPodView,
|
||||
viewFn: NewPod,
|
||||
listFn: resource.NewPodList,
|
||||
colorerFn: podColorer,
|
||||
}
|
||||
|
|
@ -195,7 +188,7 @@ func coreRes(vv viewers) {
|
|||
enterFn: showSAPolicy,
|
||||
}
|
||||
vv["v1/services"] = viewer{
|
||||
viewFn: newSvcView,
|
||||
viewFn: NewService,
|
||||
listFn: resource.NewServiceList,
|
||||
}
|
||||
vv["v1/configmaps"] = viewer{
|
||||
|
|
@ -210,7 +203,7 @@ func coreRes(vv viewers) {
|
|||
colorerFn: pvcColorer,
|
||||
}
|
||||
vv["v1/secrets"] = viewer{
|
||||
viewFn: newSecretView,
|
||||
viewFn: NewSecret,
|
||||
listFn: resource.NewSecretList,
|
||||
}
|
||||
vv["v1/endpoints"] = viewer{
|
||||
|
|
@ -221,7 +214,7 @@ func coreRes(vv viewers) {
|
|||
colorerFn: evColorer,
|
||||
}
|
||||
vv["v1/replicationcontrollers"] = viewer{
|
||||
viewFn: newScalableResourceView,
|
||||
viewFn: NewScalableResource,
|
||||
listFn: resource.NewReplicationControllerList,
|
||||
colorerFn: rsColorer,
|
||||
}
|
||||
|
|
@ -234,50 +227,50 @@ func miscRes(vv viewers) {
|
|||
vv["contexts"] = viewer{
|
||||
gvr: "contexts",
|
||||
kind: "Contexts",
|
||||
viewFn: newContextView,
|
||||
viewFn: NewContext,
|
||||
listFn: resource.NewContextList,
|
||||
colorerFn: ctxColorer,
|
||||
}
|
||||
vv["users"] = viewer{
|
||||
gvr: "users",
|
||||
viewFn: newSubjectView,
|
||||
viewFn: NewSubject,
|
||||
}
|
||||
vv["groups"] = viewer{
|
||||
gvr: "groups",
|
||||
viewFn: newSubjectView,
|
||||
viewFn: NewSubject,
|
||||
}
|
||||
vv["portforwards"] = viewer{
|
||||
gvr: "portforwards",
|
||||
viewFn: newForwardView,
|
||||
viewFn: NewPortForward,
|
||||
}
|
||||
vv["benchmarks"] = viewer{
|
||||
gvr: "benchmarks",
|
||||
viewFn: newBenchView,
|
||||
viewFn: NewBench,
|
||||
}
|
||||
vv["screendumps"] = viewer{
|
||||
gvr: "screendumps",
|
||||
viewFn: newDumpView,
|
||||
viewFn: NewScreenDump,
|
||||
}
|
||||
}
|
||||
|
||||
func appsRes(vv viewers) {
|
||||
vv["apps/v1/deployments"] = viewer{
|
||||
viewFn: newDeployView,
|
||||
viewFn: NewDeploy,
|
||||
listFn: resource.NewDeploymentList,
|
||||
colorerFn: dpColorer,
|
||||
}
|
||||
vv["apps/v1/replicasets"] = viewer{
|
||||
viewFn: newReplicaSetView,
|
||||
viewFn: NewReplicaSet,
|
||||
listFn: resource.NewReplicaSetList,
|
||||
colorerFn: rsColorer,
|
||||
}
|
||||
vv["apps/v1/statefulsets"] = viewer{
|
||||
viewFn: newStatefulSetView,
|
||||
viewFn: NewStatefulSet,
|
||||
listFn: resource.NewStatefulSetList,
|
||||
colorerFn: stsColorer,
|
||||
}
|
||||
vv["apps/v1/daemonsets"] = viewer{
|
||||
viewFn: newDaemonSetView,
|
||||
viewFn: NewDaemonSet,
|
||||
listFn: resource.NewDaemonSetList,
|
||||
colorerFn: dpColorer,
|
||||
}
|
||||
|
|
@ -324,11 +317,11 @@ func netRes(vv viewers) {
|
|||
|
||||
func batchRes(vv viewers) {
|
||||
vv["batch/v1beta1/cronjobs"] = viewer{
|
||||
viewFn: newCronJobView,
|
||||
viewFn: NewCronJob,
|
||||
listFn: resource.NewCronJobList,
|
||||
}
|
||||
vv["batch/v1/jobs"] = viewer{
|
||||
viewFn: newJobView,
|
||||
viewFn: NewJob,
|
||||
listFn: resource.NewJobList,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,513 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/k9s/internal/ui/dialog"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// EnvFn represent the current view exposed environment.
|
||||
type envFn func() K9sEnv
|
||||
|
||||
// Resource represents a generic resource viewer.
|
||||
type Resource struct {
|
||||
*MasterDetail
|
||||
|
||||
namespaces map[int]string
|
||||
list resource.List
|
||||
cancelFn context.CancelFunc
|
||||
path *string
|
||||
colorerFn ui.ColorerFunc
|
||||
decorateFn decorateFn
|
||||
envFn envFn
|
||||
gvr string
|
||||
currentNS string
|
||||
}
|
||||
|
||||
// NewResource returns a new viewer.
|
||||
func NewResource(title, gvr string, list resource.List) *Resource {
|
||||
return &Resource{
|
||||
MasterDetail: NewMasterDetail(),
|
||||
list: list,
|
||||
gvr: gvr,
|
||||
}
|
||||
}
|
||||
|
||||
// Init watches all running pods in given namespace
|
||||
func (r *Resource) Init(ctx context.Context) {
|
||||
r.MasterDetail.Init(ctx)
|
||||
r.envFn = r.defaultK9sEnv
|
||||
|
||||
table := r.masterPage()
|
||||
table.setFilterFn(r.filterResource)
|
||||
colorer := ui.DefaultColorer
|
||||
if r.colorerFn != nil {
|
||||
colorer = r.colorerFn
|
||||
}
|
||||
table.SetColorerFn(colorer)
|
||||
row, _ := table.GetSelection()
|
||||
if row == 0 && table.GetRowCount() > 0 {
|
||||
table.Select(1, 0)
|
||||
}
|
||||
r.DumpPages()
|
||||
|
||||
r.refresh()
|
||||
}
|
||||
|
||||
// Start initializes updates.
|
||||
func (r *Resource) Start() {
|
||||
r.Stop()
|
||||
var ctx context.Context
|
||||
ctx, r.cancelFn = context.WithCancel(context.Background())
|
||||
r.update(ctx)
|
||||
}
|
||||
|
||||
// Stop terminates updates.
|
||||
func (r *Resource) Stop() {
|
||||
if r.cancelFn != nil {
|
||||
r.cancelFn()
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the component name.
|
||||
func (r *Resource) Name() string {
|
||||
return r.list.GetName()
|
||||
}
|
||||
|
||||
// Hints returns the current viewer hints
|
||||
func (r *Resource) Hints() model.MenuHints {
|
||||
if r.CurrentPage() == nil {
|
||||
return nil
|
||||
}
|
||||
if c, ok := r.CurrentPage().Item.(model.Hinter); ok {
|
||||
return c.Hints()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Resource) setColorerFn(f ui.ColorerFunc) {
|
||||
r.colorerFn = f
|
||||
}
|
||||
|
||||
func (r *Resource) setDecorateFn(f decorateFn) {
|
||||
r.decorateFn = f
|
||||
}
|
||||
|
||||
func (r *Resource) filterResource(sel string) {
|
||||
r.list.SetLabelSelector(sel)
|
||||
r.refresh()
|
||||
}
|
||||
|
||||
func (r *Resource) update(ctx context.Context) {
|
||||
go func(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Debug().Msgf("%s updater canceled!", r.list.GetName())
|
||||
return
|
||||
case <-time.After(time.Duration(r.app.Config.K9s.GetRefreshRate()) * time.Second):
|
||||
r.app.QueueUpdateDraw(func() {
|
||||
r.refresh()
|
||||
})
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
}
|
||||
|
||||
func (r *Resource) backCmd(*tcell.EventKey) *tcell.EventKey {
|
||||
r.switchPage("master")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Resource) switchPage(p string) {
|
||||
log.Debug().Msgf("Switching page to %s", p)
|
||||
if _, ok := r.CurrentPage().Item.(*Table); ok {
|
||||
r.Stop()
|
||||
}
|
||||
|
||||
r.SwitchToPage(p)
|
||||
|
||||
if _, ok := r.CurrentPage().Item.(*Table); ok {
|
||||
r.Start()
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Actions...
|
||||
|
||||
func (r *Resource) cpCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !r.masterPage().RowSelected() {
|
||||
return evt
|
||||
}
|
||||
|
||||
_, n := namespaced(r.masterPage().GetSelectedItem())
|
||||
log.Debug().Msgf("Copied selection to clipboard %q", n)
|
||||
r.app.Flash().Info("Current selection copied to clipboard...")
|
||||
if err := clipboard.WriteAll(n); err != nil {
|
||||
r.app.Flash().Err(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Resource) enterCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
// If in command mode run filter otherwise enter function.
|
||||
if r.masterPage().filterCmd(evt) == nil || !r.masterPage().RowSelected() {
|
||||
return nil
|
||||
}
|
||||
|
||||
f := r.defaultEnter
|
||||
if r.enterFn != nil {
|
||||
f = r.enterFn
|
||||
}
|
||||
f(r.app, r.list.GetNamespace(), r.list.GetName(), r.masterPage().GetSelectedItem())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Resource) refreshCmd(*tcell.EventKey) *tcell.EventKey {
|
||||
r.app.Flash().Info("Refreshing...")
|
||||
r.refresh()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Resource) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !r.masterPage().RowSelected() {
|
||||
return evt
|
||||
}
|
||||
|
||||
sel := r.masterPage().GetSelectedItems()
|
||||
var msg string
|
||||
if len(sel) > 1 {
|
||||
msg = fmt.Sprintf("Delete %d selected %s?", len(sel), r.list.GetName())
|
||||
} else {
|
||||
msg = fmt.Sprintf("Delete %s %s?", r.list.GetName(), sel[0])
|
||||
}
|
||||
dialog.ShowDelete(r.Pages, msg, func(cascade, force bool) {
|
||||
r.masterPage().ShowDeleted()
|
||||
if len(sel) > 1 {
|
||||
r.app.Flash().Infof("Delete %d selected %s", len(sel), r.list.GetName())
|
||||
} else {
|
||||
r.app.Flash().Infof("Delete resource %s %s", r.list.GetName(), sel[0])
|
||||
}
|
||||
for _, res := range sel {
|
||||
if err := r.list.Resource().Delete(res, cascade, force); err != nil {
|
||||
r.app.Flash().Errf("Delete failed with %s", err)
|
||||
} else {
|
||||
deletePortForward(r.app.forwarders, res)
|
||||
}
|
||||
}
|
||||
r.refresh()
|
||||
}, func() {
|
||||
r.switchPage("master")
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Resource) markCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !r.masterPage().RowSelected() {
|
||||
return evt
|
||||
}
|
||||
r.masterPage().ToggleMark()
|
||||
r.refresh()
|
||||
r.app.Draw()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deletePortForward(ff map[string]forwarder, sel string) {
|
||||
for k, f := range ff {
|
||||
tokens := strings.Split(k, ":")
|
||||
if tokens[0] == sel {
|
||||
log.Debug().Msgf("Deleting associated portForward %s", k)
|
||||
f.Stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resource) defaultEnter(app *App, ns, _, selection string) {
|
||||
if !r.list.Access(resource.DescribeAccess) {
|
||||
return
|
||||
}
|
||||
|
||||
yaml, err := r.list.Resource().Describe(r.gvr, selection)
|
||||
if err != nil {
|
||||
r.app.Flash().Errf("Describe command failed: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
details := r.detailsPage()
|
||||
details.setCategory("Describe")
|
||||
details.setTitle(selection)
|
||||
details.SetTextColor(r.app.Styles.FgColor())
|
||||
details.SetText(colorizeYAML(r.app.Styles.Views().Yaml, yaml))
|
||||
details.ScrollToBeginning()
|
||||
r.switchPage("details")
|
||||
}
|
||||
|
||||
func (r *Resource) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !r.masterPage().RowSelected() {
|
||||
return evt
|
||||
}
|
||||
r.defaultEnter(r.app, r.list.GetNamespace(), r.list.GetName(), r.masterPage().GetSelectedItem())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Resource) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !r.masterPage().RowSelected() {
|
||||
return evt
|
||||
}
|
||||
|
||||
sel := r.masterPage().GetSelectedItem()
|
||||
raw, err := r.list.Resource().Marshal(sel)
|
||||
if err != nil {
|
||||
r.app.Flash().Errf("Unable to marshal resource %s", err)
|
||||
return evt
|
||||
}
|
||||
details := r.detailsPage()
|
||||
details.setCategory("YAML")
|
||||
details.setTitle(sel)
|
||||
details.SetTextColor(r.app.Styles.FgColor())
|
||||
details.SetText(colorizeYAML(r.app.Styles.Views().Yaml, raw))
|
||||
details.ScrollToBeginning()
|
||||
r.app.Content.Push(details)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Resource) editCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !r.masterPage().RowSelected() {
|
||||
return evt
|
||||
}
|
||||
|
||||
r.Stop()
|
||||
{
|
||||
ns, po := namespaced(r.masterPage().GetSelectedItem())
|
||||
args := make([]string, 0, 10)
|
||||
args = append(args, "edit")
|
||||
args = append(args, r.list.GetName())
|
||||
args = append(args, "-n", ns)
|
||||
args = append(args, "--context", r.app.Config.K9s.CurrentContext)
|
||||
if cfg := r.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" {
|
||||
args = append(args, "--kubeconfig", *cfg)
|
||||
}
|
||||
runK(true, r.app, append(args, po)...)
|
||||
}
|
||||
r.Start()
|
||||
|
||||
return evt
|
||||
}
|
||||
|
||||
func (r *Resource) setNamespace(ns string) {
|
||||
if r.list.Namespaced() {
|
||||
r.list.SetNamespace(ns)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resource) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
i, _ := strconv.Atoi(string(evt.Rune()))
|
||||
ns := r.namespaces[i]
|
||||
if ns == "" {
|
||||
ns = resource.AllNamespace
|
||||
}
|
||||
if r.currentNS == ns {
|
||||
return nil
|
||||
}
|
||||
|
||||
r.app.switchNS(ns)
|
||||
r.setNamespace(ns)
|
||||
r.app.Flash().Infof("Viewing namespace `%s`...", ns)
|
||||
r.refresh()
|
||||
r.masterPage().UpdateTitle()
|
||||
r.masterPage().SelectRow(1, true)
|
||||
r.app.CmdBuff().Reset()
|
||||
if err := r.app.Config.SetActiveNamespace(r.currentNS); err != nil {
|
||||
log.Error().Err(err).Msg("Config save failed!")
|
||||
}
|
||||
r.app.Config.Save()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Resource) refresh() {
|
||||
if r.CurrentPage() == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := r.CurrentPage().Item.(*Table); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
r.refreshActions()
|
||||
if r.list.Namespaced() {
|
||||
r.list.SetNamespace(r.currentNS)
|
||||
}
|
||||
if err := r.list.Reconcile(r.app.informer, r.path); err != nil {
|
||||
r.app.Flash().Err(err)
|
||||
}
|
||||
data := r.list.Data()
|
||||
if r.decorateFn != nil {
|
||||
data = r.decorateFn(data)
|
||||
}
|
||||
r.masterPage().Update(data)
|
||||
}
|
||||
|
||||
func (r *Resource) namespaceActions(aa ui.KeyActions) {
|
||||
if !r.list.Access(resource.NamespaceAccess) {
|
||||
return
|
||||
}
|
||||
r.namespaces = make(map[int]string, config.MaxFavoritesNS)
|
||||
// User can't list namespace. Don't offer a choice.
|
||||
if r.app.Conn() == nil || r.app.Conn().CheckListNSAccess() != nil {
|
||||
return
|
||||
}
|
||||
aa[tcell.Key(ui.NumKeys[0])] = ui.NewKeyAction(resource.AllNamespace, r.switchNamespaceCmd, true)
|
||||
r.namespaces[0] = resource.AllNamespace
|
||||
index := 1
|
||||
for _, n := range r.app.Config.FavNamespaces() {
|
||||
if n == resource.AllNamespace {
|
||||
continue
|
||||
}
|
||||
aa[tcell.Key(ui.NumKeys[index])] = ui.NewKeyAction(n, r.switchNamespaceCmd, true)
|
||||
r.namespaces[index] = n
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resource) refreshActions() {
|
||||
aa := ui.KeyActions{
|
||||
ui.KeyC: ui.NewKeyAction("Copy", r.cpCmd, false),
|
||||
tcell.KeyEnter: ui.NewKeyAction("Enter", r.enterCmd, false),
|
||||
tcell.KeyCtrlR: ui.NewKeyAction("Refresh", r.refreshCmd, false),
|
||||
}
|
||||
aa[ui.KeySpace] = ui.NewKeyAction("Mark", r.markCmd, true)
|
||||
r.namespaceActions(aa)
|
||||
r.defaultActions(aa)
|
||||
|
||||
if r.list.Access(resource.EditAccess) {
|
||||
aa[ui.KeyE] = ui.NewKeyAction("Edit", r.editCmd, true)
|
||||
}
|
||||
if r.list.Access(resource.DeleteAccess) {
|
||||
aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", r.deleteCmd, true)
|
||||
}
|
||||
if r.list.Access(resource.ViewAccess) {
|
||||
aa[ui.KeyY] = ui.NewKeyAction("YAML", r.viewCmd, true)
|
||||
}
|
||||
if r.list.Access(resource.DescribeAccess) {
|
||||
aa[ui.KeyD] = ui.NewKeyAction("Describe", r.describeCmd, true)
|
||||
}
|
||||
r.customActions(aa)
|
||||
|
||||
t := r.masterPage()
|
||||
t.AddActions(aa)
|
||||
}
|
||||
|
||||
func (r *Resource) customActions(aa ui.KeyActions) {
|
||||
pp := config.NewPlugins()
|
||||
if err := pp.Load(); err != nil {
|
||||
log.Warn().Msgf("No plugin configuration found")
|
||||
return
|
||||
}
|
||||
|
||||
for k, plugin := range pp.Plugin {
|
||||
if !in(plugin.Scopes, r.list.GetName()) {
|
||||
continue
|
||||
}
|
||||
key, err := asKey(plugin.ShortCut)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Unable to map shortcut to a key")
|
||||
continue
|
||||
}
|
||||
_, ok := aa[key]
|
||||
if ok {
|
||||
log.Error().Err(fmt.Errorf("Doh! you are trying to overide an existing command `%s", k)).Msg("Invalid shortcut")
|
||||
continue
|
||||
}
|
||||
aa[key] = ui.NewKeyAction(
|
||||
plugin.Description,
|
||||
r.execCmd(plugin.Command, plugin.Background, plugin.Args...),
|
||||
true)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resource) execCmd(bin string, bg bool, args ...string) ui.ActionHandler {
|
||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !r.masterPage().RowSelected() {
|
||||
return evt
|
||||
}
|
||||
|
||||
var (
|
||||
env = r.envFn()
|
||||
aa = make([]string, len(args))
|
||||
err error
|
||||
)
|
||||
for i, a := range args {
|
||||
aa[i], err = env.envFor(a)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Args match failed")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if run(true, r.app, bin, bg, aa...) {
|
||||
r.app.Flash().Info("Custom CMD launched!")
|
||||
} else {
|
||||
r.app.Flash().Info("Custom CMD failed!")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resource) defaultK9sEnv() K9sEnv {
|
||||
ns, n := namespaced(r.masterPage().GetSelectedItem())
|
||||
ctx, err := r.app.Conn().Config().CurrentContextName()
|
||||
if err != nil {
|
||||
ctx = "n/a"
|
||||
}
|
||||
cluster, err := r.app.Conn().Config().CurrentClusterName()
|
||||
if err != nil {
|
||||
cluster = "n/a"
|
||||
}
|
||||
user, err := r.app.Conn().Config().CurrentUserName()
|
||||
if err != nil {
|
||||
user = "n/a"
|
||||
}
|
||||
groups, err := r.app.Conn().Config().CurrentGroupNames()
|
||||
if err != nil {
|
||||
groups = []string{"n/a"}
|
||||
}
|
||||
var cfg string
|
||||
kcfg := r.app.Conn().Config().Flags().KubeConfig
|
||||
if kcfg != nil && *kcfg != "" {
|
||||
cfg = *kcfg
|
||||
}
|
||||
|
||||
env := K9sEnv{
|
||||
"NAMESPACE": ns,
|
||||
"NAME": n,
|
||||
"CONTEXT": ctx,
|
||||
"CLUSTER": cluster,
|
||||
"USER": user,
|
||||
"GROUPS": strings.Join(groups, ","),
|
||||
"KUBECONFIG": cfg,
|
||||
}
|
||||
|
||||
row := r.masterPage().GetRow()
|
||||
for i, r := range row {
|
||||
env["COL"+strconv.Itoa(i)] = r
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue