package views import ( "context" "fmt" "path" "regexp" "strconv" "strings" "sync" "time" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/resource" "github.com/gdamore/tcell" "github.com/k8sland/tview" log "github.com/sirupsen/logrus" ) 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 update sync.Mutex list resource.List extraActionsFn func(keyActions) decorateDataFn func(resource.TableData) resource.TableData } ) 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(), } tv := newTableView(app, v.title, list.SortFn()) { tv.SetColorer(c) tv.table.SetSelectionChangedFunc(v.selChanged) } v.AddPage(v.list.GetName(), tv, true, true) var xray details if list.HasXRay() { xray = newXrayView(app) } else { xray = newYamlView(app) } xray.setActions(keyActions{ tcell.KeyEscape: {description: "Back", action: v.back}, }) details := newDetailsView() details.setActions(keyActions{ tcell.KeyEscape: {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.Debugf("%s watcher canceled!", v.title) return case <-time.After(time.Duration(initTick) * time.Second): v.refresh() initTick = float64(config.Root.K9s.RefreshRate) } } }(ctx) v.refreshActions() if tv, ok := v.CurrentPage().Item.(*tableView); ok { tv.table.Select(0, 0) } } func (v *resourceView) getTitle() string { return v.title } 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) xRay(*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) describe(*tcell.EventKey) { if !v.rowSelected() { return } selected := v.selectedItem selected = strings.Replace(selected, "+", "", -1) selected = strings.Replace(selected, "(*)", "", -1) raw, err := v.list.Resource().Describe(v.title, selected) if err != nil { v.app.flash(flashErr, "Unable to describe this resource", err.Error()) log.Error(err) return } var re = regexp.MustCompile(`(?m:(^(.+)$))`) str := re.ReplaceAllString(string(raw), `[aqua]$1`) details := v.GetPrimitive("details").(*detailsView) details.ScrollToBeginning() details.setCategory("DESC") details.SetText(str) details.setTitle(selected) v.switchPage("details") } 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(`(?m:([\w|\.|"|\-|\/|\@]+):(.*)$)`) str := re.ReplaceAllString(string(raw), `[aqua]$1: [white]$2`) details := v.GetPrimitive("details").(*detailsView) details.ScrollToBeginning() details.setCategory("YAML") 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) { i, _ := strconv.Atoi(string(evt.Rune())) ns := v.namespaces[i] v.doSwitchNamespace(ns) } func (v *resourceView) doSwitchNamespace(ns string) { v.update.Lock() { if ns == noSelection { ns = resource.AllNamespace } v.selectedNS = ns v.app.flash(flashInfo, fmt.Sprintf("Viewing `%s namespace...", ns)) v.list.SetNamespace(v.selectedNS) } v.update.Unlock() v.refresh() v.selectItem(0, 0) v.getTV().resetTitle() v.getTV().table.Select(0, 0) v.app.cmdBuff.reset() config.Root.SetActiveNamespace(v.selectedNS) config.Root.Save() } // Utils... func (v *resourceView) refresh() { if _, ok := v.CurrentPage().Item.(*tableView); !ok { return } v.update.Lock() { if v.list.Namespaced() { v.list.SetNamespace(v.selectedNS) } if err := v.list.Reconcile(); err != nil { v.app.flash(flashErr, err.Error()) } data := v.list.Data() if v.decorateDataFn != nil { data = v.decorateDataFn(data) } v.getTV().update(data) v.refreshActions() v.app.infoView.refresh() v.app.Draw() } v.update.Unlock() } func (v *resourceView) getTV() *tableView { if tv, ok := v.GetPrimitive(v.list.GetName()).(*tableView); ok { return tv } return nil } 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.table.GetCell(r, 0).Text) case resource.AllNamespaces: v.selectedItem = path.Join( strings.TrimSpace(t.table.GetCell(r, 0).Text), strings.TrimSpace(t.table.GetCell(r, 1).Text), ) default: v.selectedItem = path.Join( v.selectedNS, strings.TrimSpace(t.table.GetCell(r, 0).Text), ) } } func (v *resourceView) switchPage(p string) { v.update.Lock() { 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.update.Unlock() } 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(resource.AllNamespaces) if err != nil { v.app.flash(flashErr, "Unable to retrieve namespaces", err.Error()) return } if v.list.Namespaced() && !v.list.AllNamespaces() { if !config.InNSList(nn, v.list.GetNamespace()) { v.list.SetNamespace(resource.DefaultNamespace) } } aa := keyActions{} if v.list.Access(resource.NamespaceAccess) { v.namespaces = make(map[int]string, config.MaxFavoritesNS) for i, n := range config.Root.FavNamespaces() { aa[tcell.Key(numKeys[i])] = newKeyHandler(n, v.switchNamespace) v.namespaces[i] = n } } if v.list.Access(resource.EditAccess) { aa[KeyE] = newKeyHandler("Edit", v.edit) } if v.list.Access(resource.DeleteAccess) { aa[tcell.KeyCtrlD] = newKeyHandler("Delete", v.delete) } if v.list.Access(resource.ViewAccess) { aa[KeyV] = newKeyHandler("View", v.view) } if v.list.Access(resource.DescribeAccess) { aa[KeyD] = newKeyHandler("Describe", v.describe) } aa[KeyHelp] = newKeyHandler("Help", v.app.noop) if v.extraActionsFn != nil { v.extraActionsFn(aa) } t := v.getTV() t.setActions(aa) v.app.setHints(t.hints()) }