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  }