table.go

  1package logs
  2
  3import (
  4	"fmt"
  5	"slices"
  6	"strings"
  7
  8	"github.com/charmbracelet/bubbles/v2/key"
  9	"github.com/charmbracelet/bubbles/v2/table"
 10	tea "github.com/charmbracelet/bubbletea/v2"
 11	"github.com/charmbracelet/crush/internal/logging"
 12	"github.com/charmbracelet/crush/internal/pubsub"
 13	"github.com/charmbracelet/crush/internal/tui/layout"
 14	"github.com/charmbracelet/crush/internal/tui/styles"
 15	"github.com/charmbracelet/crush/internal/tui/util"
 16	"github.com/charmbracelet/lipgloss/v2"
 17)
 18
 19type TableComponent interface {
 20	util.Model
 21	layout.Sizeable
 22	layout.Bindings
 23}
 24
 25type tableCmp struct {
 26	table table.Model
 27	logs  []logging.LogMessage
 28}
 29
 30type selectedLogMsg logging.LogMessage
 31
 32func (i *tableCmp) Init() tea.Cmd {
 33	i.logs = logging.List()
 34	i.setRows()
 35	return nil
 36}
 37
 38func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 39	var cmds []tea.Cmd
 40	switch msg := msg.(type) {
 41	case pubsub.Event[logging.LogMessage]:
 42		return i, func() tea.Msg {
 43			if msg.Type == pubsub.CreatedEvent {
 44				rows := i.table.Rows()
 45				for _, row := range rows {
 46					if row[1] == msg.Payload.ID {
 47						return nil // If the log already exists, do not add it again
 48					}
 49				}
 50				i.logs = append(i.logs, msg.Payload)
 51				i.table.SetRows(
 52					append(
 53						[]table.Row{
 54							logToRow(msg.Payload),
 55						},
 56						i.table.Rows()...,
 57					),
 58				)
 59			}
 60			return selectedLogMsg(msg.Payload)
 61		}
 62	}
 63	t, cmd := i.table.Update(msg)
 64	cmds = append(cmds, cmd)
 65	i.table = t
 66
 67	cmds = append(cmds, func() tea.Msg {
 68		for _, log := range logging.List() {
 69			if log.ID == i.table.SelectedRow()[1] {
 70				// If the selected row matches the log ID, return the selected log message
 71				return selectedLogMsg(log)
 72			}
 73		}
 74		return nil
 75	})
 76	return i, tea.Batch(cmds...)
 77}
 78
 79func (i *tableCmp) View() tea.View {
 80	t := styles.CurrentTheme()
 81	defaultStyles := table.DefaultStyles()
 82
 83	// Header styling
 84	defaultStyles.Header = defaultStyles.Header.
 85		Foreground(t.Primary).
 86		Bold(true).
 87		BorderStyle(lipgloss.NormalBorder()).
 88		BorderBottom(true).
 89		BorderForeground(t.Border)
 90
 91	// Selected row styling
 92	defaultStyles.Selected = defaultStyles.Selected.
 93		Foreground(t.FgSelected).
 94		Background(t.Primary).
 95		Bold(false)
 96
 97	// Cell styling
 98	defaultStyles.Cell = defaultStyles.Cell.
 99		Foreground(t.FgBase)
100
101	i.table.SetStyles(defaultStyles)
102	return tea.NewView(i.table.View())
103}
104
105func (i *tableCmp) GetSize() (int, int) {
106	return i.table.Width(), i.table.Height()
107}
108
109func (i *tableCmp) SetSize(width int, height int) tea.Cmd {
110	i.table.SetWidth(width)
111	i.table.SetHeight(height)
112
113	columnWidth := (width - 10) / 4
114	i.table.SetColumns([]table.Column{
115		{
116			Title: "Level",
117			Width: 10,
118		},
119		{
120			Title: "ID",
121			Width: columnWidth,
122		},
123		{
124			Title: "Time",
125			Width: columnWidth,
126		},
127		{
128			Title: "Message",
129			Width: columnWidth,
130		},
131		{
132			Title: "Attributes",
133			Width: columnWidth,
134		},
135	})
136	return nil
137}
138
139func (i *tableCmp) BindingKeys() []key.Binding {
140	return layout.KeyMapToSlice(i.table.KeyMap)
141}
142
143func (i *tableCmp) setRows() {
144	rows := []table.Row{}
145
146	slices.SortFunc(i.logs, func(a, b logging.LogMessage) int {
147		if a.Time.Before(b.Time) {
148			return -1
149		}
150		if a.Time.After(b.Time) {
151			return 1
152		}
153		return 0
154	})
155
156	for _, log := range i.logs {
157		rows = append(rows, logToRow(log))
158	}
159	i.table.SetRows(rows)
160}
161
162func logToRow(log logging.LogMessage) table.Row {
163	// Format attributes as JSON string
164	var attrStr string
165	if len(log.Attributes) > 0 {
166		var parts []string
167		for _, attr := range log.Attributes {
168			parts = append(parts, fmt.Sprintf(`{"Key":"%s","Value":"%s"}`, attr.Key, attr.Value))
169		}
170		attrStr = "[" + strings.Join(parts, ",") + "]"
171	}
172
173	// Format time with relative time
174	timeStr := log.Time.Format("2006-01-05 15:04:05 UTC")
175	relativeTime := getRelativeTime(log.Time)
176	fullTimeStr := timeStr + " " + relativeTime
177
178	return table.Row{
179		strings.ToUpper(log.Level),
180		log.ID,
181		fullTimeStr,
182		log.Message,
183		attrStr,
184	}
185}
186
187func NewLogsTable() TableComponent {
188	columns := []table.Column{
189		{Title: "Level"},
190		{Title: "ID"},
191		{Title: "Time"},
192		{Title: "Message"},
193		{Title: "Attributes"},
194	}
195
196	tableModel := table.New(
197		table.WithColumns(columns),
198	)
199	tableModel.Focus()
200	return &tableCmp{
201		table: tableModel,
202	}
203}