package views import ( "context" "fmt" "path" "regexp" "strconv" "strings" "time" "github.com/gdamore/tcell" "github.com/k8sland/k9s/resource" "github.com/k8sland/k9s/resource/k8s" "github.com/k8sland/tview" log "github.com/sirupsen/logrus" "k8s.io/api/core/v1" ) const noSelection = "" type ( details interface { tview.Primitive setTitle(string) clear() setActions(keyActions) update(resource.Properties) } resourceView struct { *tview.Pages app *appView title string selectedItem string namespaces map[int]string selectedNS string suspendUpdate bool list resource.List extraActionsFn func(keyActions) } ) func newResourceView(title string, app *appView, list resource.List, c colorerFn) resourceViewer { v := resourceView{ app: app, title: title, list: list, selectedNS: list.GetNamespace(), Pages: tview.NewPages(), } table := newTableView(v.title, list.SortFn()) table.SetColorer(c) table.SetSelectionChangedFunc(v.selChanged) v.AddPage(v.list.GetName(), table, true, true) var xray details if list.HasXRay() { xray = newXrayView(app) } else { xray = newYamlView(app) } xray.setActions(keyActions{ tcell.KeyCtrlB: {description: "Back", action: v.back}, }) details := newDetailsView() details.setActions(keyActions{ tcell.KeyCtrlB: {description: "Back", action: v.back}, }) v.AddPage("details", details, true, false) v.AddPage("xray", xray, true, false) return &v } // Init watches all running pods in given namespace func (v *resourceView) init(ctx context.Context, ns string) { details := v.GetPrimitive("xray").(details) details.clear() v.selectedItem, v.selectedNS = noSelection, ns go func(ctx context.Context) { initTick := 0.1 for { select { case <-ctx.Done(): log.Printf("%s watcher canceled!", v.title) return case <-time.After(time.Duration(initTick) * time.Second): if !v.isSuspended() { v.refresh() } initTick = float64(v.app.refreshRate) } } }(ctx) v.refreshActions() v.CurrentPage().Item.(*tableView).Select(0, 0) } func (v *resourceView) selChanged(r, c int) { v.selectItem(r, c) } func (v *resourceView) colorFn(f colorerFn) { v.getTV().SetColorer(f) } // Protocol... // Hints fetch menu hints func (v *resourceView) hints() hints { return v.CurrentPage().Item.(hinter).hints() } // Actions... func (v *resourceView) back(*tcell.EventKey) { v.switchPage(v.list.GetName()) } func (v *resourceView) delete(*tcell.EventKey) { if !v.rowSelected() { return } v.getTV().setDeleted() v.app.flash(flashInfo, fmt.Sprintf("Deleting %s %s", v.list.GetName(), v.selectedItem)) if err := v.list.Resource().Delete(v.selectedItem); err != nil { v.app.flash(flashErr, "Boom!", err.Error()) } v.selectedItem = noSelection } func (v *resourceView) describe(*tcell.EventKey) { details := v.GetPrimitive("xray").(details) details.clear() if !v.rowSelected() { return } props, err := v.list.Describe(v.selectedItem) if err != nil { v.app.flash(flashErr, "Unable to get xray fields", err.Error()) return } details.update(props) details.setTitle(fmt.Sprintf(" %s ", v.selectedItem)) v.switchPage("xray") } func (v *resourceView) view(*tcell.EventKey) { if !v.rowSelected() { return } raw, err := v.list.Resource().Marshal(v.selectedItem) if err != nil { v.app.flash(flashErr, "Unable to marshal resource", err.Error()) log.Error(err) return } var re = regexp.MustCompile(`([\w|\.|"|\-|\/|\@]+):(.*)`) str := re.ReplaceAllString(string(raw), `[aqua]$1: [white]$2`) details := v.GetPrimitive("details").(*detailsView) details.SetText(str) details.setTitle(v.selectedItem) v.switchPage("details") } func (v *resourceView) edit(*tcell.EventKey) { if !v.rowSelected() { return } v.app.flash(flashInfo, fmt.Sprintf("Editing %s %s", v.title, v.selectedItem)) ns, s := namespaced(v.selectedItem) run(v.app, "edit", v.list.GetName(), "-n", ns, s) return } func (v *resourceView) switchNamespace(evt *tcell.EventKey) { v.suspend() { i, _ := strconv.Atoi(string(evt.Rune())) ns := v.namespaces[i] v.selectedNS = ns if ns == noSelection { ns = "all" } v.app.flash(flashInfo, fmt.Sprintf("Viewing `%s namespace...", ns)) v.list.SetNamespace(v.selectedNS) v.refresh() } v.resume() v.selectedItem = noSelection v.getTV().resetTitle() v.app.resetCmd() } // Utils... func (v *resourceView) suspend() { v.suspendUpdate = true } func (v *resourceView) resume() { v.suspendUpdate = false } func (v *resourceView) isSuspended() bool { return v.suspendUpdate } func (v *resourceView) refresh() { if _, ok := v.CurrentPage().Item.(*tableView); !ok { return } v.list.SetNamespace(v.selectedNS) if err := v.list.Reconcile(); err != nil { v.app.flash(flashErr, err.Error()) } v.refreshActions() v.getTV().update(v.list.Data()) v.app.infoView.refresh() v.app.Draw() } func (v *resourceView) getTV() *tableView { return v.GetPrimitive(v.list.GetName()).(*tableView) } func (v *resourceView) getSelectedItem() string { return v.selectedItem } func (v *resourceView) selectItem(r, c int) { if r == 0 { v.selectedItem = noSelection return } t := v.getTV() switch v.list.GetNamespace() { case resource.NotNamespaced: v.selectedItem = strings.TrimSpace(t.GetCell(r, 0).Text) case resource.AllNamespaces: v.selectedItem = path.Join( strings.TrimSpace(t.GetCell(r, 0).Text), strings.TrimSpace(t.GetCell(r, 1).Text), ) default: v.selectedItem = path.Join( v.selectedNS, strings.TrimSpace(t.GetCell(r, 0).Text), ) } } func (v *resourceView) switchPage(p string) { v.suspend() { v.SwitchToPage(p) h := v.GetPrimitive(p).(hinter) v.selectedNS = v.list.GetNamespace() v.app.setHints(h.hints()) v.app.SetFocus(v.CurrentPage().Item) } v.resume() } func (v *resourceView) rowSelected() bool { item := v.getSelectedItem() return item != noSelection } func namespaced(n string) (string, string) { ns, po := path.Split(n) return strings.Trim(ns, "/"), po } func (v *resourceView) refreshActions() { if _, ok := v.CurrentPage().Item.(*tableView); !ok { return } nn, err := k8s.NewNamespace().List(defaultNS) if err != nil { v.app.flash(flashErr, "Unable to retrieve namespaces", err.Error()) return } aa := keyActions{} if v.list.Access(resource.NamespaceAccess) { v.namespaces = make(map[int]string, len(nn)) var i int aa[tcell.Key(numKeys[i])] = newKeyHandler("all", v.switchNamespace) v.namespaces[i] = resource.AllNamespaces i++ for _, n := range nn { nsp := n.(v1.Namespace) v.namespaces[i] = nsp.Name aa[tcell.Key(numKeys[i])] = newKeyHandler(nsp.Name, v.switchNamespace) i++ } } if v.list.Access(resource.EditAccess) { aa[tcell.KeyCtrlE] = newKeyHandler("Edit", v.edit) } if v.list.Access(resource.DeleteAccess) { aa[tcell.KeyCtrlD] = newKeyHandler("Delete", v.delete) } if v.list.Access(resource.ViewAccess) { aa[tcell.KeyCtrlV] = newKeyHandler("View", v.view) } if v.list.Access(resource.DescribeAccess) { aa[tcell.KeyCtrlX] = newKeyHandler("Describe", v.describe) } if v.extraActionsFn != nil { v.extraActionsFn(aa) } t := v.getTV() t.setActions(aa) v.app.setHints(t.hints()) }