(cli): Add helper function for drawing table

This commit is contained in:
Ken Hibino
2022-05-17 14:40:14 -07:00
parent b6d8bd1412
commit 4e0da9fb55

View File

@@ -287,6 +287,11 @@ func drawDash(s tcell.Screen, style tcell.Style, state *dashState) {
d.NL() // empty line d.NL() // empty line
// TODO: Draw HELP body // TODO: Draw HELP body
} }
if flagDebug {
d.Println(fmt.Sprintf("DEBUG: rowIdx = %d", state.rowIdx), style)
d.Println(fmt.Sprintf("DEBUG: selectedQueue = %s", state.selectedQueue), style)
d.Println(fmt.Sprintf("DEBUG: view = %v", state.view), style)
}
d.GoToBottom() d.GoToBottom()
drawFooter(d, style, state) drawFooter(d, style, state)
} }
@@ -405,70 +410,6 @@ func maxwidth(names []string) int {
return max return max
} }
const colBuffer = 4 // extra buffer between columns
type columnAlignment int
const (
alignRight columnAlignment = iota
alignLeft
)
type column struct {
name string
width int
align columnAlignment
displayValues []string // TODO: Can we use these displayValues to display stuff?
}
func newColumn(name string, align columnAlignment) *column {
return &column{
name: name,
width: runewidth.StringWidth(name),
align: align,
}
}
func (c *column) accommodate(v string) {
c.displayValues = append(c.displayValues, v)
if w := runewidth.StringWidth(v); w > c.width {
c.width = w
}
}
type table struct {
cols []*column
}
// QueueInfoFormatter exposes API to return display values for QueueInfo properties.
type QueueInfoFormatter struct {
q *asynq.QueueInfo
}
func (f *QueueInfoFormatter) Queue() string { return f.q.Queue }
func (f *QueueInfoFormatter) Size() string { return strconv.Itoa(f.q.Size) }
func (f *QueueInfoFormatter) Processed() string { return strconv.Itoa(f.q.Processed) }
func (f *QueueInfoFormatter) Failed() string { return strconv.Itoa(f.q.Failed) }
func (f *QueueInfoFormatter) State() string {
if f.q.Paused {
return "PAUSED"
}
return "RUN"
}
func (f *QueueInfoFormatter) Latency() string {
return f.q.Latency.String()
}
func (f *QueueInfoFormatter) ErrorRate() string {
return "0.23%" // TODO: Implement this
}
func (f *QueueInfoFormatter) MemoryUsage() string {
return ByteCount(f.q.MemoryUsage)
}
// ByteCount converts the given bytes into human readable string // ByteCount converts the given bytes into human readable string
func ByteCount(b int64) string { func ByteCount(b int64) string {
const unit = 1000 const unit = 1000
@@ -483,95 +424,86 @@ func ByteCount(b int64) string {
} }
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp]) return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
} }
func drawQueueTable(d *ScreenDrawer, style tcell.Style, state *dashState) { func drawQueueTable(d *ScreenDrawer, style tcell.Style, state *dashState) {
columns := []*column{ colConfigs := []*columnConfig[*asynq.QueueInfo]{
newColumn("Queue", alignLeft), {"Queue", alignLeft, func(q *asynq.QueueInfo) string { return q.Queue }},
newColumn("State", alignLeft), {"State", alignLeft, func(q *asynq.QueueInfo) string {
newColumn("Size", alignRight), if q.Paused {
newColumn("Latency", alignRight), return "PAUSED"
newColumn("MemoryUsage", alignRight), } else {
newColumn("Processed", alignRight), return "RUN"
newColumn("Failed", alignRight), }
newColumn("ErrorRate", alignRight), }},
{"Size", alignRight, func(q *asynq.QueueInfo) string { return strconv.Itoa(q.Size) }},
{"Latency", alignRight, func(q *asynq.QueueInfo) string { return q.Latency.String() }},
{"MemoryUsage", alignRight, func(q *asynq.QueueInfo) string { return ByteCount(q.MemoryUsage) }},
{"Processed", alignRight, func(q *asynq.QueueInfo) string { return strconv.Itoa(q.Processed) }},
{"Failed", alignRight, func(q *asynq.QueueInfo) string { return strconv.Itoa(q.Failed) }},
{"ErrorRate", alignRight, func(q *asynq.QueueInfo) string { return "0.23%" /* TODO: implement this */ }},
} }
drawTable(d, style, colConfigs, state.queues, state.rowIdx-1)
}
// Adjust the column widths to accomodate the values type columnAlignment int
for _, q := range state.queues {
f := QueueInfoFormatter{q} const (
for _, col := range columns { alignRight columnAlignment = iota
switch col.name { alignLeft
case "Queue": )
col.accommodate(f.Queue())
case "State": type columnConfig[V any] struct {
col.accommodate(f.State()) name string
case "Size": alignment columnAlignment
col.accommodate(f.Size()) displayFn func(v V) string
case "MemoryUsage": }
col.accommodate(f.MemoryUsage())
case "Latency": type column[V any] struct {
col.accommodate(f.Latency()) *columnConfig[V]
case "Processed": width int
col.accommodate(f.Processed()) }
case "Failed":
col.accommodate(f.Failed()) // Helper to draw a table.
case "ErrorRate": func drawTable[V any](d *ScreenDrawer, style tcell.Style, configs []*columnConfig[V], data []V, highlightRowIdx int) {
col.accommodate(f.ErrorRate()) const colBuffer = 4 // extra buffer between columns
cols := make([]*column[V], len(configs))
for i, cfg := range configs {
cols[i] = &column[V]{cfg, runewidth.StringWidth(cfg.name)}
}
// adjust the column width to accommodate the widest value.
for _, v := range data {
for _, col := range cols {
if w := runewidth.StringWidth(col.displayFn(v)); col.width < w {
col.width = w
} }
} }
} }
// print header
// Header
headerStyle := style.Background(tcell.ColorDimGray).Foreground(tcell.ColorWhite) headerStyle := style.Background(tcell.ColorDimGray).Foreground(tcell.ColorWhite)
width, _ := d.Screen().Size() for _, col := range cols {
var b strings.Builder if col.alignment == alignLeft {
for _, col := range columns { d.Print(rpad(col.name, col.width+colBuffer), headerStyle)
if col.align == alignRight {
b.WriteString(lpad(col.name, col.width+colBuffer))
} else { } else {
b.WriteString(rpad(col.name, col.width+colBuffer)) d.Print(lpad(col.name, col.width+colBuffer), headerStyle)
} }
} }
b.WriteString(strings.Repeat(" ", width-b.Len())) // span the full width d.FillLine(' ', headerStyle)
d.Println(b.String(), headerStyle) // print body
for i, v := range data {
// Body
for i, q := range state.queues {
rowStyle := style rowStyle := style
if state.rowIdx == i+1 { if highlightRowIdx == i {
rowStyle = style.Background(tcell.ColorDarkOliveGreen) rowStyle = style.Background(tcell.ColorDarkOliveGreen)
} }
f := QueueInfoFormatter{q} for _, col := range cols {
for _, col := range columns { if col.alignment == alignLeft {
switch col.name { d.Print(rpad(col.displayFn(v), col.width+colBuffer), rowStyle)
case "Queue": } else {
d.Print(rpad(f.Queue(), col.width+colBuffer), rowStyle) d.Print(lpad(col.displayFn(v), col.width+colBuffer), rowStyle)
case "State":
d.Print(rpad(f.State(), col.width+colBuffer), rowStyle)
case "Size":
d.Print(lpad(f.Size(), col.width+colBuffer), rowStyle)
case "MemoryUsage":
d.Print(lpad(f.MemoryUsage(), col.width+colBuffer), rowStyle)
case "Latency":
d.Print(lpad(f.Latency(), col.width+colBuffer), rowStyle)
case "Processed":
d.Print(lpad(f.Processed(), col.width+colBuffer), rowStyle)
case "Failed":
d.Print(lpad(f.Failed(), col.width+colBuffer), rowStyle)
case "ErrorRate":
d.Print(lpad(f.ErrorRate(), col.width+colBuffer), rowStyle)
} }
} }
d.FillLine(' ', rowStyle) d.FillLine(' ', rowStyle)
} }
if flagDebug {
d.Println(fmt.Sprintf("DEBUG: rowIdx = %d", state.rowIdx), style)
d.Println(fmt.Sprintf("DEBUG: selectedQueue = %s", state.selectedQueue), style)
d.Println(fmt.Sprintf("DEBUG: view = %v", state.view), style)
}
} }
/*** Screen Drawer ***/ /*** Screen Drawer ***/