node view tlc. add cordon/uncordon/drain options

mine
derailed 2020-03-10 20:59:38 -06:00
parent 98fea647b9
commit bdd4ecff20
26 changed files with 409 additions and 366 deletions

View File

@ -28,6 +28,7 @@ var (
version, commit, date = "dev", "dev", "n/a"
k9sFlags *config.Flags
k8sFlags *genericclioptions.ConfigFlags
demoMode = new(bool)
rootCmd = &cobra.Command{
Use: appName,
@ -38,8 +39,8 @@ var (
)
func init() {
const falseFlag = "false"
rootCmd.AddCommand(versionCmd(), infoCmd())
initTransientFlags()
initK9sFlags()
initK8sFlags()
@ -51,10 +52,10 @@ func init() {
if err := flag.Set("stderrthreshold", "fatal"); err != nil {
log.Error().Err(err)
}
if err := flag.Set("alsologtostderr", falseFlag); err != nil {
if err := flag.Set("alsologtostderr", "false"); err != nil {
log.Error().Err(err)
}
if err := flag.Set("logtostderr", falseFlag); err != nil {
if err := flag.Set("logtostderr", "false"); err != nil {
log.Error().Err(err)
}
}
@ -105,6 +106,10 @@ func loadConfiguration() *config.Config {
log.Warn().Msg("Unable to locate K9s config. Generating new configuration...")
}
log.Debug().Msgf("DEMO MODE %#v", demoMode)
if demoMode != nil {
k9sCfg.SetDemoMode(*demoMode)
}
if *k9sFlags.RefreshRate != config.DefaultRefreshRate {
k9sCfg.K9s.OverrideRefreshRate(*k9sFlags.RefreshRate)
}
@ -161,6 +166,15 @@ func parseLevel(level string) zerolog.Level {
}
}
func initTransientFlags() {
rootCmd.Flags().BoolVar(
demoMode,
"demo",
false,
"Enable demo mode to show keyboard commands",
)
}
func initK9sFlags() {
k9sFlags = config.NewFlags()
rootCmd.Flags().IntVarP(

View File

@ -79,7 +79,16 @@ func (g GVR) GV() schema.GroupVersion {
}
}
// GVR returns a a full schema representation.
// GVK returns a full schema representation.
func (g GVR) GVK() schema.GroupVersionKind {
return schema.GroupVersionKind{
Group: g.G(),
Version: g.V(),
Kind: g.R(),
}
}
// GVR returns a full schema representation.
func (g GVR) GVR() schema.GroupVersionResource {
return schema.GroupVersionResource{
Group: g.G(),
@ -88,7 +97,7 @@ func (g GVR) GVR() schema.GroupVersionResource {
}
}
// GR returns a a full schema representation.
// GR returns a full schema representation.
func (g GVR) GR() *schema.GroupResource {
return &schema.GroupResource{
Group: g.G(),

View File

@ -206,15 +206,12 @@ func (m *MetricsServer) FetchPodMetrics(fqn string) (*mv1beta1.PodMetrics, error
return mx, err
}
var key = FQN(ns, "pods")
if entry, ok := m.cache.Get(key); ok {
if list, ok := entry.(*mv1beta1.PodMetricsList); ok && list != nil {
for _, m := range list.Items {
if FQN(m.Namespace, m.Name) == fqn {
return &m, nil
}
}
if entry, ok := m.cache.Get(fqn); ok {
pmx, ok := entry.(*mv1beta1.PodMetrics)
if !ok {
return nil, fmt.Errorf("expecting podmetrics but got %T", entry)
}
return pmx, nil
}
client, err := m.MXDial()
@ -225,7 +222,7 @@ func (m *MetricsServer) FetchPodMetrics(fqn string) (*mv1beta1.PodMetrics, error
if err != nil {
return mx, err
}
m.cache.Add(key, mx, mxCacheExpiry)
m.cache.Add(fqn, mx, mxCacheExpiry)
return mx, nil
}

View File

@ -49,6 +49,7 @@ type (
K9s *K9s `yaml:"k9s"`
client client.Connection
settings KubeSettings
demoMode bool
}
)
@ -57,6 +58,16 @@ func NewConfig(ks KubeSettings) *Config {
return &Config{K9s: NewK9s(), settings: ks}
}
// DemoMode returns true if demo mode is active, false otherwise.
func (c *Config) DemoMode() bool {
return c.demoMode
}
// SetDemoMode sets the demo mode.
func (c *Config) SetDemoMode(b bool) {
c.demoMode = b
}
// Refine the configuration based on cli args.
func (c *Config) Refine(flags *genericclioptions.ConfigFlags) error {
cfg, err := flags.ToRawKubeConfigLoader().RawConfig()

View File

@ -3,6 +3,7 @@ package dao
import (
"context"
"fmt"
"io"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
@ -12,11 +13,15 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
"k8s.io/kubectl/pkg/drain"
"k8s.io/kubectl/pkg/scheme"
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
)
var (
_ Accessor = (*Node)(nil)
_ Accessor = (*Node)(nil)
_ NodeMaintainer = (*Node)(nil)
)
// NodeMetricsFunc retrieves node metrics.
@ -27,6 +32,75 @@ type Node struct {
Resource
}
// ToggleCordon toggles cordon/uncordon a node.
func (n *Node) ToggleCordon(path string, cordon bool) error {
o, err := n.Get(context.Background(), path)
if err != nil {
return err
}
h, err := drain.NewCordonHelperFromRuntimeObject(o, scheme.Scheme, n.gvr.GVK())
if err != nil {
return err
}
if !h.UpdateIfRequired(cordon) {
if cordon {
return fmt.Errorf("node is already cordoned")
}
return fmt.Errorf("node is already uncordoned")
}
err, patchErr := h.PatchOrReplace(n.Factory.Client().DialOrDie())
if patchErr != nil {
return patchErr
}
if err != nil {
return err
}
return nil
}
func (o DrainOptions) toDrainHelper(k kubernetes.Interface, w io.Writer) drain.Helper {
return drain.Helper{
Client: k,
GracePeriodSeconds: o.GracePeriodSeconds,
Timeout: o.Timeout,
DeleteLocalData: o.DeleteLocalData,
IgnoreAllDaemonSets: o.IgnoreAllDaemonSets,
Out: w,
ErrOut: w,
}
}
// Drain drains a node.
func (n *Node) Drain(path string, opts DrainOptions, w io.Writer) error {
_ = n.ToggleCordon(path, true)
h := opts.toDrainHelper(n.Factory.Client().DialOrDie(), w)
dd, errs := h.GetPodsForDeletion(path)
if len(errs) != 0 {
for _, e := range errs {
if _, err := h.ErrOut.Write([]byte(e.Error() + "\n")); err != nil {
return err
}
}
return errs[0]
}
if err := h.DeleteOrEvictPods(dd.Pods()); err != nil {
return err
}
fmt.Fprintf(h.Out, "Node %s drained!", path)
return nil
}
// Get returns a node resource.
func (n *Node) Get(_ context.Context, path string) (runtime.Object, error) {
return FetchNode(n.Factory, path)
}
// List returns a collection of node resources.
func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
labels, ok := ctx.Value(internal.KeyLabels).(string)
@ -66,15 +140,27 @@ func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
// ----------------------------------------------------------------------------
// Helpers...
// FetchNodes retrieves all nodes.
func FetchNodes(f Factory, labelsSel string) (*v1.NodeList, error) {
var list v1.NodeList
auth, err := f.Client().CanI("", "v1/nodes", []string{client.ListVerb})
// FetchNode retrieves a node.
func FetchNode(f Factory, path string) (*v1.Node, error) {
auth, err := f.Client().CanI("", "v1/nodes", []string{"get"})
if err != nil {
return &list, err
return nil, err
}
if !auth {
return &list, fmt.Errorf("user is not authorized to list nodes")
return nil, fmt.Errorf("user is not authorized to list nodes")
}
return f.Client().DialOrDie().CoreV1().Nodes().Get(path, metav1.GetOptions{})
}
// FetchNodes retrieves all nodes.
func FetchNodes(f Factory, labelsSel string) (*v1.NodeList, error) {
auth, err := f.Client().CanI("", "v1/nodes", []string{client.ListVerb})
if err != nil {
return nil, err
}
if !auth {
return nil, fmt.Errorf("user is not authorized to list nodes")
}
return f.Client().DialOrDie().CoreV1().Nodes().List(metav1.ListOptions{

View File

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

View File

@ -40,6 +40,7 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
client.NewGVR("portforwards"): &PortForward{},
client.NewGVR("v1/services"): &Service{},
client.NewGVR("v1/pods"): &Pod{},
client.NewGVR("v1/nodes"): &Node{},
client.NewGVR("apps/v1/deployments"): &Deployment{},
client.NewGVR("apps/v1/daemonsets"): &DaemonSet{},
client.NewGVR("extensions/v1beta1/daemonsets"): &DaemonSet{},

View File

@ -2,6 +2,8 @@ package dao
import (
"context"
"io"
"time"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/watch"
@ -70,6 +72,24 @@ type Accessor interface {
GVR() string
}
// DrainOptions tracks drain attributes.
type DrainOptions struct {
GracePeriodSeconds int
Timeout time.Duration
IgnoreAllDaemonSets bool
DeleteLocalData bool
Force bool
}
// NodeMaintainer performs node maintenance operations.
type NodeMaintainer interface {
// ToggleCordon toggles cordon/uncordon a node.
ToggleCordon(path string, cordon bool) error
// Drain drains the given node.
Drain(path string, opts DrainOptions, w io.Writer) error
}
// Loggable represents resources with logs.
type Loggable interface {
// TaiLogs streams resource logs.

View File

@ -275,7 +275,6 @@ func applyFilter(q string, lines []string) ([]string, error) {
}
func (l *Log) fireLogBuffChanged(lines []string) {
log.Debug().Msgf("FIRE-BUFF-CHNGED")
filtered, err := applyFilter(l.filter, lines)
if err != nil {
l.fireLogError(err)
@ -307,7 +306,7 @@ func (l *Log) fireLogCleared() {
// ----------------------------------------------------------------------------
// Helpers...
// BOZO!! Log timestamps.
// BOZO!!
// func showTimes(lines []string, show bool) []string {
// filtered := make([]string, 0, len(lines))
// for _, l := range lines {

View File

@ -224,6 +224,10 @@ func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, err
}
func (t *Table) reconcile(ctx context.Context) error {
defer func(ti time.Time) {
log.Debug().Msgf("Elapsed %v %v", t.gvr, time.Since(ti))
}(time.Now())
meta := t.resourceMeta()
var (
oo []runtime.Object

View File

@ -107,13 +107,25 @@ func (Node) diagnose(ss []string) error {
if len(ss) == 0 {
return nil
}
var ready bool
for _, s := range ss {
if s == "" {
continue
}
if s == "SchedulingDisabled" {
return errors.New("node is cordoned")
}
if s == "Ready" {
return nil
ready = true
}
}
return errors.New("node is not ready")
if !ready {
return errors.New("node is not ready")
}
return nil
}
// ----------------------------------------------------------------------------

View File

@ -31,7 +31,7 @@ func ShowConfirm(pages *ui.Pages, title, msg string, ack confirmFunc, cancel can
cancel()
})
modal := tview.NewModalForm(" <"+title+"> ", f)
modal := tview.NewModalForm("<"+title+">", f)
modal.SetText(msg)
modal.SetDoneFunc(func(int, string) {
dismissConfirm(pages)

View File

@ -31,6 +31,7 @@ func NewFlash(app *App) *Flash {
TextView: tview.NewTextView(),
}
f.SetTextColor(tcell.ColorAqua)
f.SetDynamicColors(true)
f.SetTextAlign(tview.AlignCenter)
f.SetBorderPadding(0, 0, 1, 1)
f.app.Styles.AddListener(&f)

View File

@ -104,9 +104,9 @@ func (a *App) Init(version string, rate int) error {
main := tview.NewFlex().SetDirection(tview.FlexRow)
main.AddItem(a.statusIndicator(), 1, 1, false)
main.AddItem(flash, 1, 1, false)
main.AddItem(a.Content, 0, 10, true)
main.AddItem(a.Crumbs(), 1, 1, false)
main.AddItem(flash, 1, 1, false)
a.Main.AddPage("main", main, true, false)
a.Main.AddPage("splash", ui.NewSplash(a.Styles, version), true, true)
@ -116,6 +116,7 @@ func (a *App) Init(version string, rate int) error {
}
func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey {
displayKey(a, a.InCmdMode(), evt)
key := evt.Key()
if key == tcell.KeyRune {
if a.CmdBuff().IsActive() && evt.Modifiers() == tcell.ModNone {
@ -154,7 +155,7 @@ func (a *App) toggleHeader(flag bool) {
}
if a.showHeader {
flex.RemoveItemAtIndex(0)
flex.AddItemAtIndex(0, a.buildHeader(), 8, 1, false)
flex.AddItemAtIndex(0, a.buildHeader(), 7, 1, false)
} else {
flex.RemoveItemAtIndex(0)
flex.AddItemAtIndex(0, a.statusIndicator(), 1, 1, false)

View File

@ -192,7 +192,6 @@ func (b *Browser) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
}
cmd := b.SearchBuff().String()
b.App().Flash().Info("Clearing filter...")
b.SearchBuff().Reset()
if ui.IsLabelSelector(cmd) {

View File

@ -128,6 +128,7 @@ func (d *Details) bindKeys() {
}
func (d *Details) keyboard(evt *tcell.EventKey) *tcell.EventKey {
displayKey(d.app, d.cmdBuff.InCmdMode(), evt)
key := evt.Key()
if key == tcell.KeyUp || key == tcell.KeyDown {
return evt
@ -248,7 +249,6 @@ func (d *Details) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
if d.app.InCmdMode() {
return evt
}
d.app.Flash().Info("Filter mode activated.")
d.cmdBuff.SetActive(true)
return nil
@ -281,7 +281,6 @@ func (d *Details) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if d.cmdBuff.String() != "" {
d.model.ClearFilter()
}
d.app.Flash().Info("Clearing filter...")
d.cmdBuff.SetActive(false)
d.cmdBuff.Reset()
d.updateTitle()

View File

@ -0,0 +1,103 @@
package view
import (
"strconv"
"time"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview"
)
const drainKey = "drain"
// DrainFunc represents a drain callback function.
type DrainFunc func(v ResourceViewer, path string, opts dao.DrainOptions)
// ShowDrain pops a node drain dialog.
func ShowDrain(view ResourceViewer, path string, defaults dao.DrainOptions, okFn DrainFunc) {
styles := view.App().Styles
f := tview.NewForm()
f.SetItemPadding(0)
f.SetButtonsAlign(tview.AlignCenter).
SetButtonBackgroundColor(styles.BgColor()).
SetButtonTextColor(styles.FgColor()).
SetLabelColor(styles.K9s.Info.FgColor.Color()).
SetFieldTextColor(styles.K9s.Info.SectionColor.Color())
var opts dao.DrainOptions
f.AddInputField("GracePeriod:", strconv.Itoa(defaults.GracePeriodSeconds), 0, nil, func(v string) {
a, err := asIntOpt(v)
if err != nil {
view.App().Flash().Err(err)
return
}
view.App().Flash().Clear()
opts.GracePeriodSeconds = a
})
f.AddInputField("Timeout:", defaults.Timeout.String(), 0, nil, func(v string) {
a, err := asDurOpt(v)
if err != nil {
view.App().Flash().Err(err)
return
}
view.App().Flash().Clear()
opts.Timeout = a
})
f.AddCheckbox("Ignore DaemonSets:", defaults.IgnoreAllDaemonSets, func(v bool) {
opts.IgnoreAllDaemonSets = v
})
f.AddCheckbox("Delete Local Data:", defaults.DeleteLocalData, func(v bool) {
opts.DeleteLocalData = v
})
f.AddCheckbox("Force:", defaults.Force, func(v bool) {
opts.Force = v
})
pages := view.App().Content.Pages
f.AddButton("Cancel", func() {
DismissDrain(view, pages)
})
f.AddButton("OK", func() {
DismissDrain(view, pages)
okFn(view, path, opts)
})
modal := tview.NewModalForm("<Drain>", f)
modal.SetText(path)
modal.SetDoneFunc(func(_ int, b string) {
DismissDrain(view, pages)
})
pages.AddPage(drainKey, modal, false, true)
pages.ShowPage(drainKey)
view.App().SetFocus(pages.GetPrimitive(drainKey))
}
// DismissDrain dismiss the port forward dialog.
func DismissDrain(v ResourceViewer, p *ui.Pages) {
p.RemovePage(drainKey)
v.App().SetFocus(p.CurrentPage().Item)
}
// ----------------------------------------------------------------------------
// Helpers...
func asDurOpt(v string) (time.Duration, error) {
d, err := time.ParseDuration(v)
if err != nil {
return 0, err
}
return d, nil
}
func asIntOpt(v string) (int, error) {
i, err := strconv.Atoi(v)
if err != nil {
return 0, err
}
return i, nil
}

View File

@ -50,6 +50,7 @@ func generalEnv(a *App) K9sEnv {
}
func defaultK9sEnv(a *App, sel string, row render.Row) K9sEnv {
log.Debug().Msgf("ROW %#v", row)
ns, n := client.Namespaced(sel)
env := generalEnv(a)

View File

@ -185,6 +185,7 @@ func (l *Log) bindKeys() {
}
func (l *Log) keyboard(evt *tcell.EventKey) *tcell.EventKey {
displayKey(l.app, l.cmdBuff.InCmdMode(), evt)
key := evt.Key()
if key == tcell.KeyUp || key == tcell.KeyDown {
return evt
@ -265,7 +266,6 @@ func (l *Log) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
if l.app.InCmdMode() {
return evt
}
l.app.Flash().Info("Filter mode activated.")
l.cmdBuff.SetActive(true)
return nil
@ -293,7 +293,6 @@ func (l *Log) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if l.cmdBuff.String() != "" {
l.model.ClearFilter()
}
l.app.Flash().Info("Clearing filter...")
l.cmdBuff.SetActive(false)
l.cmdBuff.Reset()
l.updateTitle()
@ -330,6 +329,7 @@ func saveData(cluster, name, data string) (string, error) {
if err != nil {
log.Error().Err(err).Msgf("LogFile create %s", path)
return "", nil
}
defer func() {
if err := file.Close(); err != nil {
@ -344,7 +344,6 @@ func saveData(cluster, name, data string) (string, error) {
}
func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey {
l.app.Flash().Info("Clearing logs...")
l.model.Clear()
return nil
}
@ -355,7 +354,7 @@ func (l *Log) textWrapCmd(*tcell.EventKey) *tcell.EventKey {
return nil
}
// BOZO! Log timestamps.
// BOZO!! Log timestamps.
// func (l *Log) toggleTimestampCmd(evt *tcell.EventKey) *tcell.EventKey {
// l.model.Clear()
// l.indicator.ToggleTimestamp()

View File

@ -16,8 +16,7 @@ type LogIndicator struct {
scrollStatus int32
fullScreen bool
textWrap bool
// BOZO!! timestamp
// showTime bool
showTime bool
}
// NewLogIndicator returns a new indicator.
@ -40,11 +39,10 @@ func (l *LogIndicator) AutoScroll() bool {
return atomic.LoadInt32(&l.scrollStatus) == 1
}
// BOZO!! Timestamp
// // Timestamp reports the current timestamp mode.
// func (l *LogIndicator) Timestamp() bool {
// return l.showTime
// }
// Timestamp reports the current timestamp mode.
func (l *LogIndicator) Timestamp() bool {
return l.showTime
}
// TextWrap reports the current wrap mode.
func (l *LogIndicator) TextWrap() bool {
@ -56,11 +54,10 @@ func (l *LogIndicator) FullScreen() bool {
return l.fullScreen
}
// BOZO!! Timestamp
// // TextWrap reports the current wrap mode.
// func (l *LogIndicator) ToggleTimestamp() {
// l.showTime = !l.showTime
// }
// TextWrap reports the current wrap mode.
func (l *LogIndicator) ToggleTimestamp() {
l.showTime = !l.showTime
}
// ToggleFullScreen toggles the screen mode.
func (l *LogIndicator) ToggleFullScreen() {

View File

@ -1,9 +1,15 @@
package view
import (
"bytes"
"fmt"
"strings"
"time"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/ui/dialog"
"github.com/gdamore/tcell"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -28,6 +34,9 @@ func (n *Node) bindKeys(aa ui.KeyActions) {
aa.Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlD)
aa.Add(ui.KeyActions{
ui.KeyY: ui.NewKeyAction("YAML", n.viewCmd, true),
ui.KeyC: ui.NewKeyAction("Cordon", n.toggleCordonCmd(true), true),
ui.KeyU: ui.NewKeyAction("Uncordon", n.toggleCordonCmd(false), true),
ui.KeyD: ui.NewKeyAction("Drain", n.drainCmd, true),
ui.KeyShiftC: ui.NewKeyAction("Sort CPU", n.GetTable().SortColCmd(cpuCol, false), false),
ui.KeyShiftM: ui.NewKeyAction("Sort MEM", n.GetTable().SortColCmd(memCol, false), false),
ui.KeyShiftX: ui.NewKeyAction("Sort CPU%", n.GetTable().SortColCmd("%CPU", false), false),
@ -39,6 +48,84 @@ func (n *Node) showPods(app *App, _ ui.Tabular, _, path string) {
showPods(app, n.GetTable().GetSelectedItem(), "", "spec.nodeName="+path)
}
func (n *Node) drainCmd(evt *tcell.EventKey) *tcell.EventKey {
path := n.GetTable().GetSelectedItem()
if path == "" {
return evt
}
defaults := dao.DrainOptions{
GracePeriodSeconds: -1,
Timeout: 5 * time.Second,
DeleteLocalData: false,
IgnoreAllDaemonSets: false,
}
ShowDrain(n, path, defaults, drainNode)
return nil
}
func drainNode(v ResourceViewer, path string, opts dao.DrainOptions) {
res, err := dao.AccessorFor(v.App().factory, v.GVR())
if err != nil {
v.App().Flash().Err(err)
return
}
m, ok := res.(dao.NodeMaintainer)
if !ok {
v.App().Flash().Err(fmt.Errorf("expecting a maintainer for %q", v.GVR()))
return
}
buff := bytes.NewBufferString("")
if err := m.Drain(path, opts, buff); err != nil {
v.App().Flash().Err(err)
return
}
lines := strings.Split(buff.String(), "\n")
for _, l := range lines {
if len(l) > 0 {
v.App().Flash().Info(l)
}
}
v.Refresh()
}
func (n *Node) toggleCordonCmd(cordon bool) func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey {
path := n.GetTable().GetSelectedItem()
if path == "" {
return evt
}
title, msg := "Confirm ", ""
if cordon {
title, msg = title+"Cordon", "Cordon "
} else {
title, msg = title+"Uncordon", "Uncordon "
}
msg += path + "?"
dialog.ShowConfirm(n.App().Content.Pages, title, msg, func() {
res, err := dao.AccessorFor(n.App().factory, n.GVR())
if err != nil {
n.App().Flash().Err(err)
return
}
m, ok := res.(dao.NodeMaintainer)
if !ok {
n.App().Flash().Err(fmt.Errorf("expecting a maintainer for %q", n.GVR()))
return
}
if err := m.ToggleCordon(path, cordon); err != nil {
n.App().Flash().Err(err)
}
n.Refresh()
}, func() {})
return nil
}
}
func (n *Node) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
path := n.GetTable().GetSelectedItem()
if path == "" {

View File

@ -208,6 +208,7 @@ func (p *Pulse) bindKeys() {
}
func (p *Pulse) keyboard(evt *tcell.EventKey) *tcell.EventKey {
displayKey(p.app, false, evt)
key := evt.Key()
if key == tcell.KeyRune {
key = tcell.Key(evt.Rune())

View File

@ -42,7 +42,7 @@ func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey {
if len(paths) > 1 {
msg = fmt.Sprintf("Restart %d deployments?", len(paths))
}
dialog.ShowConfirm(r.App().Content.Pages, "<Confirm Restart>", msg, func() {
dialog.ShowConfirm(r.App().Content.Pages, "Confirm Restart", msg, func() {
for _, path := range paths {
if err := r.restartRollout(path); err != nil {
r.App().Flash().Err(err)

View File

@ -2,6 +2,7 @@ package view
import (
"context"
"strings"
"time"
"github.com/atotto/clipboard"
@ -55,7 +56,25 @@ func (t *Table) SendKey(evt *tcell.EventKey) {
t.keyboard(evt)
}
func displayKey(a *App, isCmd bool, evt *tcell.EventKey) {
if !a.Config.DemoMode() || a.InCmdMode() || isCmd {
a.Flash().Clear()
return
}
a.Flash().Clear()
key, ok := tcell.KeyNames[evt.Key()]
if !ok {
key = string(evt.Rune())
}
if evt.Modifiers() == tcell.ModCtrl {
key = "⌃" + strings.Replace(key, "Ctrl-", "", 1)
}
a.Flash().Infof("Pressed[:springgreen:b]%s", key)
}
func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
displayKey(t.app, t.SearchBuff().InCmdMode(), evt)
key := evt.Key()
if key == tcell.KeyUp || key == tcell.KeyDown {
return evt
@ -233,7 +252,6 @@ func (t *Table) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
if t.app.InCmdMode() {
return evt
}
t.app.Flash().Info("Filter mode activated.")
t.SearchBuff().SetActive(true)
return nil

View File

@ -417,7 +417,6 @@ func (x *Xray) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
if x.app.InCmdMode() {
return evt
}
x.app.Flash().Info("Filter mode activated.")
x.CmdBuff().SetActive(true)
return nil
@ -448,8 +447,6 @@ func (x *Xray) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
x.CmdBuff().Reset()
return x.app.PrevCmd(evt)
}
x.app.Flash().Info("Clearing filter...")
x.CmdBuff().Reset()
x.model.ClearFilter()
x.Start()

313
xray.yml

File diff suppressed because one or more lines are too long