github.com/wtfutil/wtf@v0.43.0/modules/airbrake/widget.go (about) 1 package airbrake 2 3 import ( 4 "bytes" 5 "fmt" 6 "strings" 7 8 "github.com/rivo/tview" 9 "github.com/wtfutil/wtf/utils" 10 "github.com/wtfutil/wtf/view" 11 ) 12 13 const module = "Airbrake" 14 15 var emojis = map[string]string{ 16 "bug": "ð", 17 "bell with slash": "ð", 18 } 19 20 type ShowType int 21 22 const ( 23 SHOW_TITLE ShowType = iota 24 SHOW_COMPARE 25 ) 26 27 type Widget struct { 28 view.ScrollableWidget 29 30 settings *Settings 31 app *tview.Application 32 pages *tview.Pages 33 34 groups []Group 35 project *Project 36 37 showType ShowType 38 err error 39 } 40 41 type GroupJSON struct { 42 Groups []Group `json:"groups"` 43 } 44 45 type Group struct { 46 ID string `json:"id"` 47 ProjectID int64 `json:"projectId"` 48 Errors []Error 49 NoticeCount int64 `json:"noticeCount"` 50 CreatedAt string `json:"createdAt"` 51 LastNoticeAt string `json:"lastNoticeAt"` 52 Context GroupContext 53 Muted bool `json:"muted"` 54 CommentCount int64 `json:"commentCount"` 55 } 56 57 type GroupContext struct { 58 Environment string `json:"environment"` 59 Severity string `json:"severity"` 60 } 61 62 func (g *Group) Link() string { 63 return fmt.Sprintf("https://airbrake.io/projects/%d/groups/%s", g.ProjectID, g.ID) 64 } 65 66 func (g *Group) Title() string { 67 return fmt.Sprintf("%s: %s", g.Type(), g.Message()) 68 } 69 70 func (g *Group) Type() string { 71 return g.Errors[0].Type 72 } 73 74 func (g *Group) Message() string { 75 err := g.Errors[0] 76 return strings.ReplaceAll(err.Message, "\n", ". ") 77 } 78 79 func (g *Group) File() string { 80 s := fmt.Sprintf("%s:%d", g.Errors[0].Backtrace[0].File, 81 g.Errors[0].Backtrace[0].Line) 82 return reverseString(utils.Truncate(reverseString(s), 51, true)) 83 } 84 85 type Error struct { 86 Type string `json:"type"` 87 Message string `json:"message"` 88 Backtrace []StackFrame `json:"backtrace"` 89 } 90 91 type StackFrame struct { 92 File string `json:"file"` 93 Function string `json:"function"` 94 Line int64 `json:"line"` 95 } 96 97 type ProjectJSON struct { 98 Project Project `json:"project"` 99 } 100 101 type Project struct { 102 Name string `json:"name"` 103 } 104 105 func rotateShowType(showtype ShowType) ShowType { 106 returnValue := SHOW_TITLE 107 switch showtype { 108 case SHOW_TITLE: 109 returnValue = SHOW_COMPARE 110 case SHOW_COMPARE: 111 returnValue = SHOW_TITLE 112 } 113 return returnValue 114 } 115 116 func NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget { 117 widget := Widget{ 118 ScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common), 119 120 app: tviewApp, 121 settings: settings, 122 pages: pages, 123 showType: SHOW_TITLE, 124 } 125 126 widget.SetRenderFunction(widget.Render) 127 widget.initializeKeyboardControls() 128 129 return &widget 130 } 131 132 /* -------------------- Exported Functions -------------------- */ 133 134 func (widget *Widget) Refresh() { 135 if widget.Disabled() { 136 return 137 } 138 139 groups, err := groups( 140 widget.settings.projectID, 141 widget.settings.authToken) 142 if err != nil { 143 widget.err = err 144 widget.groups = nil 145 widget.SetItemCount(0) 146 } else { 147 widget.err = nil 148 widget.groups = groups 149 widget.SetItemCount(len(groups)) 150 } 151 152 project, err := project( 153 widget.settings.projectID, 154 widget.settings.authToken) 155 if err != nil { 156 widget.err = err 157 widget.project = nil 158 } else { 159 widget.err = nil 160 widget.project = project 161 } 162 163 widget.Render() 164 } 165 166 func (widget *Widget) Render() { 167 widget.Redraw(widget.content) 168 } 169 170 /* -------------------- Unexported Functions -------------------- */ 171 172 func (widget *Widget) content() (string, string, bool) { 173 if widget.err != nil { 174 return module, widget.err.Error(), true 175 } 176 177 project := widget.project 178 if project != nil && project.Name == "" { 179 return module, "No project found", true 180 } 181 182 title := fmt.Sprintf("%s %s - %s's recent errors", emojis["bug"], 183 module, project.Name) 184 185 result := widget.groups 186 if result == nil || len(widget.groups) == 0 { 187 return title, "All your errors are resolved!", false 188 } 189 190 var str string 191 for idx, g := range widget.groups { 192 rowColor := widget.RowColor(idx) 193 var row string 194 195 if widget.showType == SHOW_TITLE { 196 var buf bytes.Buffer 197 if g.Muted { 198 buf.WriteString(emojis["bell with slash"]) 199 } else { 200 buf.WriteString(" ") 201 } 202 buf.WriteString(" " + g.Title()) 203 row = fmt.Sprintf("[%s]%2d. %s[white]", rowColor, idx+1, buf.String()) 204 } else { 205 row = fmt.Sprintf( 206 "[%s]%2d. %-31s %-11s %-10s count: %-9d comments: %-2d[white]", 207 rowColor, idx+1, utils.Truncate(g.Type(), 30, true), 208 g.Context.Environment, g.Context.Severity, 209 g.NoticeCount, g.CommentCount) 210 } 211 212 str += utils.HighlightableHelper(widget.View, row, idx, len(g.Type())) 213 } 214 215 return title, str, false 216 } 217 218 func (widget *Widget) openGroup() { 219 sel := widget.GetSelected() 220 221 if sel >= 0 && widget.groups != nil && sel < len(widget.groups) { 222 group := widget.groups[sel] 223 utils.OpenFile(group.Link()) 224 } 225 } 226 227 func (widget *Widget) viewGroup() { 228 sel := widget.GetSelected() 229 230 if sel >= 0 && widget.groups != nil && sel < len(widget.groups) { 231 group := widget.groups[sel] 232 233 closeFunc := func() { 234 widget.pages.RemovePage("group info") 235 widget.app.SetFocus(widget.View) 236 } 237 238 table := newGroupInfoTable(&group).render() 239 table += utils.CenterText("Esc to close", 80) 240 241 modal := view.NewBillboardModal(table, closeFunc) 242 modal.SetTitle(fmt.Sprintf(" %s ", group.Title())) 243 244 widget.pages.AddPage("group info", modal, false, true) 245 widget.app.SetFocus(modal) 246 247 widget.app.QueueUpdateDraw(func() { 248 widget.app.Draw() 249 }) 250 } 251 } 252 253 func (widget *Widget) resolveGroup() { 254 sel := widget.GetSelected() 255 256 if sel >= 0 && widget.groups != nil && sel < len(widget.groups) { 257 group := widget.groups[sel] 258 259 closeFunc := func() { 260 widget.pages.RemovePage("resolve") 261 widget.app.SetFocus(widget.View) 262 } 263 264 var tbl *resultTable 265 err := resolveGroup(group.ProjectID, group.ID, widget.settings.authToken) 266 if err == nil { 267 tbl = newResultTable("Success", "Error Resolved") 268 widget.Refresh() 269 } else { 270 tbl = newResultTable("Error", err.Error()) 271 } 272 273 modal := view.NewBillboardModal(tbl.render(), closeFunc) 274 modal.SetTitle(fmt.Sprintf(" %s ", group.Title())) 275 276 widget.pages.AddPage("resolve", modal, false, true) 277 widget.app.SetFocus(modal) 278 279 widget.app.QueueUpdateDraw(func() { 280 widget.app.Draw() 281 }) 282 } 283 } 284 285 func (widget *Widget) muteGroup() { 286 if widget.showType != SHOW_TITLE { 287 return 288 } 289 290 sel := widget.GetSelected() 291 292 if sel >= 0 && widget.groups != nil && sel < len(widget.groups) { 293 group := widget.groups[sel] 294 if !group.Muted { 295 widget.err = muteGroup(group.ProjectID, group.ID, widget.settings.authToken) 296 widget.Refresh() 297 } 298 } 299 } 300 301 func (widget *Widget) unmuteGroup() { 302 if widget.showType != SHOW_TITLE { 303 return 304 } 305 306 sel := widget.GetSelected() 307 308 if sel >= 0 && widget.groups != nil && sel < len(widget.groups) { 309 group := widget.groups[sel] 310 if group.Muted { 311 widget.err = unmuteGroup(group.ProjectID, group.ID, widget.settings.authToken) 312 widget.Refresh() 313 } 314 } 315 } 316 317 func (widget *Widget) toggleDisplayText() { 318 widget.showType = rotateShowType(widget.showType) 319 widget.Render() 320 }