Skip to content

Commit

Permalink
(feature): add a toggleable view mode to the table panel (#34)
Browse files Browse the repository at this point in the history
Signed-off-by: Bryce Palmer <everettraven@gmail.com>
  • Loading branch information
everettraven committed Nov 17, 2023
1 parent 451ebd3 commit 8ac4411
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 88 deletions.
34 changes: 34 additions & 0 deletions pkg/charm/models/composite_help.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package models

import (
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
)

type CompositeHelpKeyMap struct {
helps []help.KeyMap
}

func NewCompositeHelpKeyMap(helps ...help.KeyMap) *CompositeHelpKeyMap {
return &CompositeHelpKeyMap{helps: helps}
}

// ShortHelp returns keybindings to be shown in the mini help view. It's part
// of the key.Map interface.
func (ch CompositeHelpKeyMap) ShortHelp() []key.Binding {
bindings := []key.Binding{}
for _, h := range ch.helps {
bindings = append(bindings, h.ShortHelp()...)
}
return bindings
}

// FullHelp returns keybindings for the expanded help view. It's part of the
// key.Map interface.
func (ch CompositeHelpKeyMap) FullHelp() [][]key.Binding {
bindings := [][]key.Binding{}
for _, h := range ch.helps {
bindings = append(bindings, h.FullHelp()...)
}
return bindings
}
20 changes: 13 additions & 7 deletions pkg/charm/models/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ import (
)

type DashboardKeyMap struct {
Help key.Binding
Quit key.Binding
TabberKeys TabberKeyMap
Help key.Binding
Quit key.Binding
}

// ShortHelp returns keybindings to be shown in the mini help view. It's part
Expand All @@ -27,8 +26,7 @@ func (k DashboardKeyMap) ShortHelp() []key.Binding {
// key.Map interface.
func (k DashboardKeyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{k.TabberKeys.TabLeft, k.TabberKeys.TabRight}, // first column
{k.Help, k.Quit}, // second column
{k.Help, k.Quit},
}
}

Expand All @@ -41,7 +39,6 @@ var DefaultDashboardKeys = DashboardKeyMap{
key.WithKeys("q", "esc", "ctrl+c"),
key.WithHelp("q, esc, ctrl+c", "quit"),
),
TabberKeys: DefaultTabberKeys,
}

type Namer interface {
Expand Down Expand Up @@ -100,5 +97,14 @@ func (d *Dashboard) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

func (d *Dashboard) View() string {
div := styles.TabGap().Render(strings.Repeat(" ", max(0, d.width-2)))
return lipgloss.JoinVertical(0, d.tabber.View(), div, d.help.View(d.keys))
return lipgloss.JoinVertical(0, d.tabber.View(), div, d.help.View(d.Help()))
}

func (d *Dashboard) Help() help.KeyMap {
return CompositeHelpKeyMap{
helps: []help.KeyMap{
d.tabber.Help(),
d.keys,
},
}
}
212 changes: 198 additions & 14 deletions pkg/charm/models/panels/table.go
Original file line number Diff line number Diff line change
@@ -1,65 +1,183 @@
package panels

import (
"bytes"
"encoding/json"
"fmt"
"io"
"sync"

"github.com/alecthomas/chroma/quick"
tbl "github.com/calyptia/go-bubble-table"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/everettraven/buoy/pkg/charm/styles"
buoytypes "github.com/everettraven/buoy/pkg/types"
"github.com/tidwall/gjson"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/cache"
"sigs.k8s.io/yaml"
)

type TableKeyMap struct {
ViewModeToggle key.Binding
}

// ShortHelp returns keybindings to be shown in the mini help view. It's part
// of the key.Map interface.
func (k TableKeyMap) ShortHelp() []key.Binding {
return []key.Binding{}
}

// FullHelp returns keybindings for the expanded help view. It's part of the
// key.Map interface.
func (k TableKeyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{k.ViewModeToggle},
}
}

var DefaultTableKeys = TableKeyMap{
ViewModeToggle: key.NewBinding(
key.WithKeys("v"),
key.WithHelp("v", "toggle viewing contents of selected resource"),
),
}

const modeView = "view"
const modeTable = "table"

type RowInfo struct {
Row tbl.Row
Identifier *types.NamespacedName
// Is this necessary? Can the index change on different iterations?
Index int
}

// Table is a tea.Model implementation
// that represents a table panel
type Table struct {
table tbl.Model
lister cache.GenericLister
scope meta.RESTScopeName
viewport viewport.Model
mode string
name string
mutex *sync.Mutex
rows map[types.UID]tbl.Row
rows map[types.UID]*RowInfo
columns []buoytypes.Column
err error
tempRows []tbl.Row
keys TableKeyMap
}

func NewTable(name string, table tbl.Model, columns []buoytypes.Column) *Table {
func NewTable(keys TableKeyMap, table *buoytypes.Table, lister cache.GenericLister, scope meta.RESTScopeName) *Table {
tblColumns := []string{}
width := 0
for _, column := range table.Columns {
tblColumns = append(tblColumns, column.Header)
width += column.Width
}

tab := tbl.New(tblColumns, 100, 10)
tab.Styles.SelectedRow = styles.TableSelectedRowStyle()

return &Table{
table: table,
name: name,
mutex: &sync.Mutex{},
rows: map[types.UID]tbl.Row{},
columns: columns,
table: tab,
viewport: viewport.New(0, 0),
scope: scope,
lister: lister,
mode: modeTable,
name: table.Name,
mutex: &sync.Mutex{},
rows: map[types.UID]*RowInfo{},
columns: table.Columns,
keys: keys,
}
}

func (m *Table) Init() tea.Cmd { return nil }
func (m *Table) Init() tea.Cmd {
return nil
}

func (m *Table) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.table.SetSize(msg.Width, msg.Height/2)
m.viewport.Width = msg.Width
m.viewport.Height = msg.Height / 2
case tea.KeyMsg:
switch {
case key.Matches(msg, DefaultTableKeys.ViewModeToggle):
switch m.mode {
case modeTable:
m.mode = modeView
vpContent, err := m.FetchContentForIndex(m.table.Cursor())
if err != nil {
m.viewport.SetContent(err.Error())
} else {
m.viewport.SetContent(vpContent)
}
case modeView:
m.mode = modeTable
m.viewport.SetContent("")
}
}
}

if len(m.tempRows) > 0 {
m.table.SetRows(m.tempRows)
m.tempRows = []tbl.Row{}
}

m.table, cmd = m.table.Update(msg)
switch m.mode {
case modeTable:
m.table, cmd = m.table.Update(msg)
case modeView:
m.viewport, cmd = m.viewport.Update(msg)
}
return m, cmd
}

func (m *Table) View() string {
if m.err != nil {
return m.err.Error()
}
return m.table.View()
switch m.mode {
case modeTable:
return m.table.View()
case modeView:
return m.viewport.View()
default:
return "?"
}
}

func (m *Table) AddOrUpdateRow(uid types.UID, row tbl.Row) {
func (m *Table) AddOrUpdate(u *unstructured.Unstructured) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.rows[uid] = row
uid := u.GetUID()
row := tbl.SimpleRow{}
for _, column := range m.Columns() {
val, err := getDotNotationValue(u.Object, column.Path)
if err != nil {
m.SetError(err)
break
}

row = append(row, fmt.Sprint(val))
}

m.rows[uid] = &RowInfo{
Row: row,
Identifier: &types.NamespacedName{Namespace: u.GetNamespace(), Name: u.GetName()},
}
m.updateRows()
}

Expand All @@ -72,8 +190,11 @@ func (m *Table) DeleteRow(uid types.UID) {

func (m *Table) updateRows() {
rows := []tbl.Row{}
for _, row := range m.rows {
rows = append(rows, row)
indice := 0
for _, rowInfo := range m.rows {
rows = append(rows, rowInfo.Row)
rowInfo.Index = indice
indice++
}
m.tempRows = rows
}
Expand All @@ -89,3 +210,66 @@ func (m *Table) Name() string {
func (m *Table) SetError(err error) {
m.err = err
}

func (m *Table) FetchContentForIndex(index int) (string, error) {
m.mutex.Lock()
defer m.mutex.Unlock()
var rowInfo *RowInfo
for _, row := range m.rows {
if row.Index == index {
rowInfo = row
break
}
}

name := rowInfo.Identifier.String()
if m.scope == meta.RESTScopeNameRoot {
name = rowInfo.Identifier.Name
}

obj, err := m.lister.Get(name)
if err != nil {
return "", fmt.Errorf("fetching definition for %q: %w", name, err)
}

itemJSON, err := obj.(*unstructured.Unstructured).MarshalJSON()
if err != nil {
return "", fmt.Errorf("error marshalling item %q: %w", name, err)
}

itemYAML, err := yaml.JSONToYAML(itemJSON)
if err != nil {
return "", fmt.Errorf("converting JSON to YAML for item %q: %w", name, err)
}

theme := "nord"
if !lipgloss.HasDarkBackground() {
theme = "monokailight"
}
rw := &bytes.Buffer{}
err = quick.Highlight(rw, string(itemYAML), "yaml", "terminal16m", theme)
if err != nil {
return "", fmt.Errorf("highlighting YAML for item %q: %w", name, err)
}
highlighted, err := io.ReadAll(rw)
if err != nil {
return "", fmt.Errorf("reading highlighted YAML for item %q: %w", name, err)
}
return string(highlighted), nil
}

func (m *Table) Help() help.KeyMap {
return m.keys
}

func getDotNotationValue(item map[string]interface{}, dotPath string) (interface{}, error) {
jsonBytes, err := json.Marshal(item)
if err != nil {
return nil, fmt.Errorf("error marshalling item to json: %w", err)
}
res := gjson.Get(string(jsonBytes), dotPath)
if !res.Exists() {
return "n/a", nil
}
return res.Value(), nil
}
Loading

0 comments on commit 8ac4411

Please sign in to comment.