diff --git a/internal/views/alias.go b/internal/views/alias.go index 036b4d67..aaf26662 100644 --- a/internal/views/alias.go +++ b/internal/views/alias.go @@ -4,11 +4,9 @@ import ( "context" "fmt" "strings" - "time" "github.com/derailed/k9s/internal/resource" "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" ) const ( @@ -30,28 +28,8 @@ func newAliasView(app *appView) *aliasView { v.colorerFn = aliasColorer v.current = app.content.GetPrimitive("main").(igniter) v.currentNS = "" + v.registerActions() } - v.actions[tcell.KeyEnter] = newKeyAction("Goto", v.gotoCmd, true) - v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd, false) - v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd, false) - v.actions[KeyShiftR] = newKeyAction("Sort Resources", v.sortResourceCmd, true) - v.actions[KeyShiftO] = newKeyAction("Sort Groups", v.sortGroupCmd, true) - - ctx, cancel := context.WithCancel(context.TODO()) - v.cancel = cancel - go func(ctx context.Context) { - for { - select { - case <-ctx.Done(): - log.Debug().Msg("Alias GR bailing out!") - return - case <-time.After(1 * time.Second): - v.update(v.hydrate()) - v.app.Draw() - } - } - }(ctx) - return &v } @@ -62,6 +40,14 @@ func (v *aliasView) init(context.Context, string) { v.resetTitle() } +func (v *aliasView) registerActions() { + v.actions[tcell.KeyEnter] = newKeyAction("Goto", v.gotoCmd, true) + v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd, false) + v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd, false) + v.actions[KeyShiftR] = newKeyAction("Sort Resources", v.sortResourceCmd, true) + v.actions[KeyShiftO] = newKeyAction("Sort Groups", v.sortGroupCmd, true) +} + func (v *aliasView) getTitle() string { return aliasTitle } diff --git a/internal/views/app.go b/internal/views/app.go index bf05a982..c1aba4a7 100644 --- a/internal/views/app.go +++ b/internal/views/app.go @@ -20,6 +20,7 @@ type ( igniter interface { tview.Primitive + getTitle() string init(ctx context.Context, ns string) } @@ -30,6 +31,7 @@ type ( resourceViewer interface { igniter + setEnterFn(enterFn) } appView struct { @@ -90,6 +92,8 @@ func NewApp(cfg *config.Config) *appView { v.actions[tcell.KeyDelete] = newKeyAction("Erase", v.eraseCmd, false) v.actions[tcell.KeyTab] = newKeyAction("Focus", v.focusCmd, false) + // v.actions[KeyO] = newKeyAction("RBAC", v.rbacCmd, false) + return &v } @@ -156,6 +160,11 @@ func (a *appView) keyboard(evt *tcell.EventKey) *tcell.EventKey { return evt } +func (a *appView) rbacCmd(evt *tcell.EventKey) *tcell.EventKey { + a.inject(newRBACView(a, "", "aa_k9s", clusterRole)) + return evt +} + func (a *appView) redrawCmd(evt *tcell.EventKey) *tcell.EventKey { a.Draw() return evt diff --git a/internal/views/colorer.go b/internal/views/colorer.go index 2e14faa4..274ef69d 100644 --- a/internal/views/colorer.go +++ b/internal/views/colorer.go @@ -33,6 +33,13 @@ func aliasColorer(string, *resource.RowEvent) tcell.Color { return tcell.ColorFuchsia } +func rbacColorer(ns string, r *resource.RowEvent) tcell.Color { + c := defaultColorer(ns, r) + + // return tcell.ColorDarkOliveGreen + return c +} + func podColorer(ns string, r *resource.RowEvent) tcell.Color { c := defaultColorer(ns, r) @@ -62,6 +69,7 @@ func podColorer(ns string, r *resource.RowEvent) tcell.Color { c = errColor } } + return c } @@ -74,6 +82,7 @@ func ctxColorer(ns string, r *resource.RowEvent) tcell.Color { if strings.Contains(strings.TrimSpace(r.Fields[0]), "*") { c = highlightColor } + return c } @@ -86,6 +95,7 @@ func pvColorer(ns string, r *resource.RowEvent) tcell.Color { if strings.TrimSpace(r.Fields[4]) != "Bound" { return errColor } + return stdColor } @@ -103,6 +113,7 @@ func pvcColorer(ns string, r *resource.RowEvent) tcell.Color { if strings.TrimSpace(r.Fields[markCol]) != "Bound" { c = errColor } + return c } @@ -119,6 +130,7 @@ func pdbColorer(ns string, r *resource.RowEvent) tcell.Color { if strings.TrimSpace(r.Fields[markCol]) != strings.TrimSpace(r.Fields[markCol+1]) { return errColor } + return stdColor } @@ -135,6 +147,7 @@ func dpColorer(ns string, r *resource.RowEvent) tcell.Color { if strings.TrimSpace(r.Fields[markCol]) != strings.TrimSpace(r.Fields[markCol+1]) { return errColor } + return stdColor } @@ -151,6 +164,7 @@ func stsColorer(ns string, r *resource.RowEvent) tcell.Color { if strings.TrimSpace(r.Fields[markCol]) != strings.TrimSpace(r.Fields[markCol+1]) { return errColor } + return stdColor } @@ -167,6 +181,7 @@ func rsColorer(ns string, r *resource.RowEvent) tcell.Color { if strings.TrimSpace(r.Fields[markCol]) != strings.TrimSpace(r.Fields[markCol+1]) { return errColor } + return stdColor } @@ -184,6 +199,7 @@ func evColorer(ns string, r *resource.RowEvent) tcell.Color { case "Killing": c = killColor } + return c } diff --git a/internal/views/command.go b/internal/views/command.go index cf5ed75e..991de100 100644 --- a/internal/views/command.go +++ b/internal/views/command.go @@ -44,7 +44,7 @@ func (c *command) run(cmd string) bool { } }() - var v igniter + var v resourceViewer switch cmd { case "q", "quit": c.app.Stop() @@ -56,12 +56,19 @@ func (c *command) run(cmd string) bool { if res, ok := resourceViews()[cmd]; ok { var r resource.List if res.listMxFn != nil { - r = res.listMxFn(c.app.conn(), k8s.NewMetricsServer(c.app.conn()), resource.DefaultNamespace) + r = res.listMxFn(c.app.conn(), + k8s.NewMetricsServer(c.app.conn()), + resource.DefaultNamespace, + ) } else { r = res.listFn(c.app.conn(), resource.DefaultNamespace) } v = res.viewFn(res.title, c.app, r, res.colorerFn) - c.app.flash(flashInfo, fmt.Sprintf("Viewing %s in namespace %s...", res.title, c.app.config.ActiveNamespace())) + if res.enterFn != nil { + v.setEnterFn(res.enterFn) + } + const fmat = "Viewing %s in namespace %s..." + c.app.flash(flashInfo, fmt.Sprintf(fmat, res.title, c.app.config.ActiveNamespace())) log.Debug().Msgf("Running command %s", cmd) c.exec(cmd, v) return true diff --git a/internal/views/help.go b/internal/views/help.go index 3326c93c..c1b13b4f 100644 --- a/internal/views/help.go +++ b/internal/views/help.go @@ -75,7 +75,7 @@ func (v *helpView) init(_ context.Context, _ string) { } fmt.Fprintf(v, "šŸ  [aqua::b]%s\n", "General") for _, h := range general { - fmt.Fprintf(v, "[pink::b]%9s [gray::]%s\n", h.key, h.description) + v.printHelp(h.key, h.description) } navigation := []helpItem{ @@ -90,7 +90,7 @@ func (v *helpView) init(_ context.Context, _ string) { } fmt.Fprintf(v, "\nšŸ¤– [aqua::b]%s\n", "View Navigation") for _, h := range navigation { - fmt.Fprintf(v, "[pink::b]%9s [gray::]%s\n", h.key, h.description) + v.printHelp(h.key, h.description) } views := []helpItem{ @@ -99,12 +99,15 @@ func (v *helpView) init(_ context.Context, _ string) { } fmt.Fprintf(v, "ļøļø\n😱 [aqua::b]%s\n", "Help") for _, h := range views { - fmt.Fprintf(v, "[pink::b]%9s [gray::]%s\n", h.key, h.description) + v.printHelp(h.key, h.description) } - v.app.setHints(v.hints()) } +func (v *helpView) printHelp(key, desc string) { + fmt.Fprintf(v, "[pink::b]%9s [white::]%s\n", key, desc) +} + func (v *helpView) hints() hints { return v.actions.toHints() } diff --git a/internal/views/helpers.go b/internal/views/helpers.go index a5b381ba..b4be66b5 100644 --- a/internal/views/helpers.go +++ b/internal/views/helpers.go @@ -46,6 +46,7 @@ func deltas(c, n string) string { if len(c) == 0 { return n } + switch strings.Compare(c, n) { case 1, -1: return delta(n) @@ -76,6 +77,7 @@ func numerical(s string) (int, bool) { func delta(s string) string { return suffix(s, "šœŸ") } + func plus(s string) string { return suffix(s, "+") } diff --git a/internal/views/rbac.go b/internal/views/rbac.go new file mode 100644 index 00000000..76cbc88e --- /dev/null +++ b/internal/views/rbac.go @@ -0,0 +1,363 @@ +package views + +import ( + "context" + "fmt" + "reflect" + "strings" + "time" + + "github.com/derailed/k9s/internal/resource" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/watch" +) + +const ( + clusterRole roleKind = iota + role + + all = "*" + rbacTitle = "RBAC" + rbacTitleFmt = " [aqua::b]%s([fuchsia::b]%s[aqua::-])" +) + +type ( + roleKind = int8 + + rbacView struct { + *tableView + + current igniter + cancel context.CancelFunc + roleType roleKind + roleName string + cache resource.RowEvents + } +) + +var ( + rbacHeaderVerbs = resource.Row{ + "GET ", + "LIST ", + "DLIST ", + "WATCH ", + "CREATE", + "PATCH ", + "UPDATE", + "DELETE", + "EXTRAS", + } + rbacHeader = append(resource.Row{"NAME", "GROUP"}, rbacHeaderVerbs...) + + k8sVerbs = []string{ + "get", + "list", + "deletecollection", + "watch", + "create", + "patch", + "update", + "delete", + } + + httpVerbs = []string{ + "get", + "post", + "put", + "patch", + "delete", + "options", + } + + httpTok8sVerbs = map[string]string{ + "post": "create", + "put": "update", + } +) + +func newRBACView(app *appView, ns, name string, kind roleKind) *rbacView { + v := rbacView{} + { + v.roleName, v.roleType = name, kind + v.tableView = newTableView(app, v.getTitle()) + v.currentNS = ns + // v.SetSelectedStyle(tcell.ColorWhite, tcell.ColorDarkSeaGreen, tcell.AttrNone) + v.colorerFn = rbacColorer + v.current = app.content.GetPrimitive("main").(igniter) + } + v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd, false) + v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd, false) + v.actions[KeyShiftO] = newKeyAction("Sort Groups", v.sortColCmd(1), true) + + ctx, cancel := context.WithCancel(context.Background()) + v.cancel = cancel + go func(ctx context.Context) { + for { + select { + case <-ctx.Done(): + log.Debug().Msg("RBAC Watch bailing out!") + return + case <-time.After(time.Duration(app.config.K9s.RefreshRate) * time.Second): + v.refresh() + v.app.Draw() + } + } + }(ctx) + + return &v +} + +// Init the view. +func (v *rbacView) init(_ context.Context, ns string) { + // v.baseTitle = v.getTitle() + v.sortCol = sortColumn{1, len(rbacHeader), true} + v.refresh() + v.app.SetFocus(v) +} + +func (v *rbacView) getTitle() string { + title := "ClusterRole" + if v.roleType == role { + title = "Role" + } + + return fmt.Sprintf(rbacTitleFmt, title, v.roleName) +} + +func (v *rbacView) refresh() { + data, err := v.reconcile(v.currentNS, v.roleName, v.roleType) + if err != nil { + log.Error().Err(err).Msgf("Unable to reconcile for %s:%d", v.roleName, v.roleType) + } + v.update(data) +} + +func (v *rbacView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { + if !v.cmdBuff.empty() { + v.cmdBuff.reset() + return nil + } + + return v.backCmd(evt) +} + +func (v *rbacView) backCmd(evt *tcell.EventKey) *tcell.EventKey { + if v.cancel != nil { + v.cancel() + } + + if v.cmdBuff.isActive() { + v.cmdBuff.reset() + } else { + v.app.inject(v.current) + } + + return nil +} + +func (v *rbacView) hints() hints { + return v.actions.toHints() +} + +func (v *rbacView) reconcile(ns, name string, kind roleKind) (resource.TableData, error) { + evts, err := v.rowEvents(ns, name, kind) + if err != nil { + return resource.TableData{}, err + } + + data := resource.TableData{ + Header: rbacHeader, + Rows: make(resource.RowEvents, len(evts)), + Namespace: resource.NotNamespaced, + } + + noDeltas := make(resource.Row, len(rbacHeader)) + if len(v.cache) == 0 { + for k, ev := range evts { + ev.Action = resource.New + ev.Deltas = noDeltas + data.Rows[k] = ev + } + v.cache = evts + + return data, nil + } + + for k, ev := range evts { + data.Rows[k] = ev + + newr := ev.Fields + if _, ok := v.cache[k]; !ok { + ev.Action, ev.Deltas = watch.Added, noDeltas + continue + } + oldr := v.cache[k].Fields + deltas := make(resource.Row, len(newr)) + if !reflect.DeepEqual(oldr, newr) { + ev.Action = watch.Modified + for i, field := range oldr { + if field != newr[i] { + deltas[i] = field + } + } + ev.Deltas = deltas + } else { + ev.Action = resource.Unchanged + ev.Deltas = noDeltas + } + } + v.cache = evts + + for k := range v.cache { + if _, ok := data.Rows[k]; !ok { + delete(v.cache, k) + } + } + return data, nil +} + +func (v *rbacView) rowEvents(ns, name string, kind roleKind) (resource.RowEvents, error) { + var ( + evts resource.RowEvents + err error + ) + + switch kind { + case clusterRole: + evts, err = v.clusterPolicies(name) + case role: + evts, err = v.namespacedPolicies(name) + default: + return evts, fmt.Errorf("Expecting clusterrole/role but found %d", kind) + } + if err != nil { + log.Error().Err(err).Msg("Unable to load CR") + return evts, err + } + + return evts, nil +} + +func toGroup(g string) string { + if g == "" { + return "v1" + } + return g +} + +func (v *rbacView) clusterPolicies(name string) (resource.RowEvents, error) { + cr, err := v.app.conn().DialOrDie().Rbac().ClusterRoles().Get(name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + + return v.parseRules(cr.Rules), nil +} + +func (v *rbacView) namespacedPolicies(path string) (resource.RowEvents, error) { + ns, na := namespaced(path) + cr, err := v.app.conn().DialOrDie().Rbac().Roles(ns).Get(na, metav1.GetOptions{}) + if err != nil { + return nil, err + } + + return v.parseRules(cr.Rules), nil +} + +func (*rbacView) parseRules(rules []rbacv1.PolicyRule) resource.RowEvents { + const ( + nameLen = 60 + groupLen = 30 + ) + + m := make(resource.RowEvents, len(rules)) + for _, r := range rules { + for _, grp := range r.APIGroups { + for _, res := range r.Resources { + k := res + if grp != "" { + k = res + "." + grp + } + for _, na := range r.ResourceNames { + n := k + "/" + na + m[n] = &resource.RowEvent{ + Fields: makeRow(resource.Pad(n, nameLen), resource.Pad(toGroup(grp), groupLen), asVerbs(r.Verbs...)), + } + } + m[k] = &resource.RowEvent{ + Fields: makeRow(resource.Pad(k, nameLen), resource.Pad(toGroup(grp), groupLen), asVerbs(r.Verbs...)), + } + } + } + for _, nres := range r.NonResourceURLs { + if nres[0] != '/' { + nres = "/" + nres + } + m[nres] = &resource.RowEvent{ + Fields: makeRow(resource.Pad(nres, nameLen), resource.Pad(resource.NAValue, groupLen), asVerbs(r.Verbs...)), + } + } + } + + return m +} + +func makeRow(res, group string, verbs []string) resource.Row { + r := make(resource.Row, 0, 12) + r = append(r, res, group) + + return append(r, verbs...) +} + +func asVerbs(verbs ...string) resource.Row { + const ( + verbLen = 4 + unknownLen = 30 + ) + + r := make(resource.Row, 0, len(k8sVerbs)+1) + for _, v := range k8sVerbs { + r = append(r, resource.Pad(toVerbIcon(hasVerb(verbs, v)), 4)) + } + + var unknowns []string + for _, v := range verbs { + if hv, ok := httpTok8sVerbs[v]; ok { + v = hv + } + if !hasVerb(k8sVerbs, v) && v != all { + unknowns = append(unknowns, v) + } + } + + return append(r, resource.Truncate(strings.Join(unknowns, ","), unknownLen)) +} + +func toVerbIcon(ok bool) string { + if ok { + return "[green::b] āœ“ [::]" + } + return "[orangered::b] 𐄂 [::]" +} + +func hasVerb(verbs []string, verb string) bool { + if len(verbs) == 1 && verbs[0] == all { + return true + } + + for _, v := range verbs { + if hv, ok := httpTok8sVerbs[v]; ok { + if hv == verb { + return true + } + } + if v == verb { + return true + } + } + + return false +} diff --git a/internal/views/rbac_test.go b/internal/views/rbac_test.go new file mode 100644 index 00000000..7c341cc2 --- /dev/null +++ b/internal/views/rbac_test.go @@ -0,0 +1,115 @@ +package views + +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{ + "*.*": resource.Row{resource.Pad("*.*", 60), resource.Pad("*", 30), ok, ok, ok, ok, ok, ok, ok, ok, ""}, + }, + }, + { + []rbacv1.PolicyRule{ + {APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"get"}}, + }, + map[string]resource.Row{ + "*.*": resource.Row{resource.Pad("*.*", 60), resource.Pad("*", 30), ok, nok, nok, nok, nok, nok, nok, nok, ""}, + }, + }, + { + []rbacv1.PolicyRule{ + {APIGroups: []string{""}, Resources: []string{"*"}, Verbs: []string{"list"}}, + }, + map[string]resource.Row{ + "*": resource.Row{resource.Pad("*", 60), resource.Pad("v1", 30), 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": resource.Row{resource.Pad("pods", 60), resource.Pad("v1", 30), nok, ok, nok, nok, nok, nok, nok, nok, ""}, + "pods/fred": resource.Row{resource.Pad("pods/fred", 60), resource.Pad("v1", 30), 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": resource.Row{resource.Pad("/fred", 60), resource.Pad("", 30), 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": resource.Row{resource.Pad("/fred", 60), resource.Pad("", 30), 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) + } + } +} diff --git a/internal/views/registrar.go b/internal/views/registrar.go index fa5eed7e..96579bd0 100644 --- a/internal/views/registrar.go +++ b/internal/views/registrar.go @@ -4,6 +4,7 @@ import ( "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/resource" "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" ) type ( @@ -11,6 +12,7 @@ type ( listFn func(c resource.Connection, ns string) resource.List listMxFn func(c resource.Connection, mx resource.MetricsServer, ns string) resource.List colorerFn func(ns string, evt *resource.RowEvent) tcell.Color + enterFn func(app *appView, ns, resource, selection string) resCmd struct { title string @@ -18,6 +20,7 @@ type ( viewFn viewFn listFn listFn listMxFn listMxFn + enterFn enterFn colorerFn colorerFn } ) @@ -72,6 +75,16 @@ func allCRDs(c k8s.Connection) map[string]k8s.APIGroup { return m } +func showRBAC(app *appView, ns, resource, selection string) { + log.Debug().Msgf("Entered FN on `%s`--%s:%s", ns, resource, selection) + kind := clusterRole + if resource == "role" { + kind = role + } + app.command.pushCmd("policies") + app.inject(newRBACView(app, ns, selection, kind)) +} + func resourceViews() map[string]resCmd { return map[string]resCmd{ "cm": { @@ -86,6 +99,7 @@ func resourceViews() map[string]resCmd { api: "rbac.authorization.k8s.io", viewFn: newResourceView, listFn: resource.NewClusterRoleList, + enterFn: showRBAC, colorerFn: defaultColorer, }, "crb": { @@ -226,6 +240,7 @@ func resourceViews() map[string]resCmd { api: "rbac.authorization.k8s.io", viewFn: newResourceView, listFn: resource.NewRoleList, + enterFn: showRBAC, colorerFn: defaultColorer, }, "rs": { diff --git a/internal/views/resource.go b/internal/views/resource.go index 32f08140..30c4befb 100644 --- a/internal/views/resource.go +++ b/internal/views/resource.go @@ -42,6 +42,7 @@ type ( selectedNS string update sync.Mutex list resource.List + enterFn enterFn extraActionsFn func(keyActions) selectedFn func() string decorateDataFn func(resource.TableData) resource.TableData @@ -124,9 +125,21 @@ func (v *resourceView) hints() hints { return v.CurrentPage().Item.(hinter).hints() } +func (v *resourceView) setEnterFn(f enterFn) { + v.enterFn = f +} + // ---------------------------------------------------------------------------- // Actions... +func (v *resourceView) enterCmd(*tcell.EventKey) *tcell.EventKey { + v.app.flash(flashInfo, "Enter pressed...") + if v.enterFn != nil { + v.enterFn(v.app, v.list.GetNamespace(), v.list.GetName(), v.selectedItem) + } + return nil +} + func (v *resourceView) refreshCmd(*tcell.EventKey) *tcell.EventKey { v.app.flash(flashInfo, "Refreshing...") v.refresh() @@ -359,6 +372,8 @@ func (v *resourceView) refreshActions() { } } + aa[tcell.KeyEnter] = newKeyAction("Enter", v.enterCmd, true) + aa[tcell.KeyCtrlR] = newKeyAction("Refresh", v.refreshCmd, false) aa[KeyHelp] = newKeyAction("Help", v.app.noopCmd, false) aa[KeyP] = newKeyAction("Previous", v.app.prevCmd, false) @@ -370,7 +385,7 @@ func (v *resourceView) refreshActions() { aa[tcell.KeyCtrlD] = newKeyAction("Delete", v.deleteCmd, true) } if v.list.Access(resource.ViewAccess) { - aa[KeyV] = newKeyAction("View", v.viewCmd, true) + aa[KeyY] = newKeyAction("YAML", v.viewCmd, true) } if v.list.Access(resource.DescribeAccess) { aa[KeyD] = newKeyAction("Describe", v.describeCmd, true) diff --git a/internal/views/table.go b/internal/views/table.go index c586d7c4..beb4d373 100644 --- a/internal/views/table.go +++ b/internal/views/table.go @@ -14,7 +14,7 @@ import ( ) const ( - titleFmt = " [aqua::b]%s[aqua::-]([fuchsia::b]%d[aqua::-]) " + titleFmt = " [aqua::b]%s[aqua::-][[fuchsia::b]%d[aqua::-]] " searchFmt = "<[green::b]/%s[aqua::]> " nsTitleFmt = " [aqua::b]%s([fuchsia::b]%s[aqua::-])[aqua::-][[aqua::b]%d[aqua::-]][aqua::-] " ) @@ -63,24 +63,8 @@ func newTableView(app *appView, title string) *tableView { v.SetSelectable(true, false) v.SetSelectedStyle(tcell.ColorBlack, tcell.ColorAqua, tcell.AttrBold) v.SetInputCapture(v.keyboard) + v.registerHandlers() } - - v.actions[KeyShiftI] = newKeyAction("Invert", v.sortInvertCmd, true) - v.actions[KeyShiftN] = newKeyAction("Sort Name", v.sortColCmd(0), true) - v.actions[KeyShiftA] = newKeyAction("Sort Age", v.sortColCmd(-1), true) - - v.actions[KeySlash] = newKeyAction("Filter Mode", v.activateCmd, false) - v.actions[tcell.KeyEscape] = newKeyAction("Filter Reset", v.resetCmd, false) - v.actions[tcell.KeyEnter] = newKeyAction("Filter", v.filterCmd, false) - - v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false) - v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false) - v.actions[tcell.KeyDelete] = newKeyAction("Erase", v.eraseCmd, false) - v.actions[KeyG] = newKeyAction("Top", app.puntCmd, false) - v.actions[KeyShiftG] = newKeyAction("Bottom", app.puntCmd, false) - v.actions[KeyB] = newKeyAction("Down", v.pageDownCmd, false) - v.actions[KeyF] = newKeyAction("Up", v.pageUpCmd, false) - return &v } @@ -128,6 +112,7 @@ func (v *tableView) pageDownCmd(evt *tcell.EventKey) *tcell.EventKey { func (v *tableView) filterCmd(evt *tcell.EventKey) *tcell.EventKey { v.cmdBuff.setActive(false) v.refresh() + return nil } @@ -309,10 +294,11 @@ func (v *tableView) doUpdate(data resource.TableData) { row++ // for k := range data.Rows { - // log.Debug().Msgf("Keys: %s", k) + // log.Debug().Msgf("Keys: `%s`", k) // } keys := v.sortRows(data) + // log.Debug().Msgf("KEYS %#v", keys) groupKeys := map[string][]string{} for _, k := range keys { // log.Debug().Msgf("RKEY: %s", k) @@ -447,6 +433,24 @@ func (v *tableView) resetTitle() { // ---------------------------------------------------------------------------- // Event listeners... +func (v *tableView) registerHandlers() { + v.actions[KeyShiftI] = newKeyAction("Invert", v.sortInvertCmd, true) + v.actions[KeyShiftN] = newKeyAction("Sort Name", v.sortColCmd(0), true) + v.actions[KeyShiftA] = newKeyAction("Sort Age", v.sortColCmd(-1), true) + + v.actions[KeySlash] = newKeyAction("Filter Mode", v.activateCmd, false) + v.actions[tcell.KeyEscape] = newKeyAction("Filter Reset", v.resetCmd, false) + v.actions[tcell.KeyEnter] = newKeyAction("Filter", v.filterCmd, false) + + v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false) + v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false) + v.actions[tcell.KeyDelete] = newKeyAction("Erase", v.eraseCmd, false) + v.actions[KeyG] = newKeyAction("Top", v.app.puntCmd, false) + v.actions[KeyShiftG] = newKeyAction("Bottom", v.app.puntCmd, false) + v.actions[KeyB] = newKeyAction("Down", v.pageDownCmd, false) + v.actions[KeyF] = newKeyAction("Up", v.pageUpCmd, false) +} + func (v *tableView) changed(s string) { }