table.go

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