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}