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}