Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feature): add a toggleable view mode to the table panel #34

Merged
merged 1 commit into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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