(cli): Add group selection table

This commit is contained in:
Ken Hibino
2022-05-20 17:18:20 -07:00
parent 2e9b5ed17e
commit 553891e837
3 changed files with 187 additions and 36 deletions

View File

@@ -29,14 +29,17 @@ const (
type State struct { type State struct {
queues []*asynq.QueueInfo queues []*asynq.QueueInfo
tasks []*asynq.TaskInfo tasks []*asynq.TaskInfo
groups []*asynq.GroupInfo
redisInfo redisInfo redisInfo redisInfo
err error err error
queueTableRowIdx int // highlighted row in queue table queueTableRowIdx int // highlighted row in queue table
taskTableRowIdx int // highlighted row in task table taskTableRowIdx int // highlighted row in task table
groupTableRowIdx int // highlighted row in group table
taskState asynq.TaskState // highlighted task state in queue details view taskState asynq.TaskState // highlighted task state in queue details view
selectedQueue *asynq.QueueInfo // queue shown on queue details view selectedQueue *asynq.QueueInfo // queue shown on queue details view
selectedGroup *asynq.GroupInfo
pageNum int // pagination page number pageNum int // pagination page number
@@ -78,6 +81,7 @@ func Run(opts Options) {
errorCh = make(chan error) errorCh = make(chan error)
queueCh = make(chan *asynq.QueueInfo) queueCh = make(chan *asynq.QueueInfo)
queuesCh = make(chan []*asynq.QueueInfo) queuesCh = make(chan []*asynq.QueueInfo)
groupsCh = make(chan []*asynq.GroupInfo)
tasksCh = make(chan []*asynq.TaskInfo) tasksCh = make(chan []*asynq.TaskInfo)
redisInfoCh = make(chan *redisInfo) redisInfoCh = make(chan *redisInfo)
) )
@@ -144,30 +148,60 @@ func Run(opts Options) {
} }
drawDash(s, baseStyle, &state, opts) drawDash(s, baseStyle, &state, opts)
} else if (ev.Key() == tcell.KeyDown || ev.Rune() == 'j') && state.view == viewTypeQueueDetails { } else if (ev.Key() == tcell.KeyDown || ev.Rune() == 'j') && state.view == viewTypeQueueDetails {
if state.taskTableRowIdx < len(state.tasks) { if shouldShowGroupTable(&state) {
state.taskTableRowIdx++ if state.groupTableRowIdx < groupPageSize(s) {
state.groupTableRowIdx++
} else {
state.groupTableRowIdx = 0 // loop back
}
} else { } else {
state.taskTableRowIdx = 0 // loop back if state.taskTableRowIdx < len(state.tasks) {
state.taskTableRowIdx++
} else {
state.taskTableRowIdx = 0 // loop back
}
} }
drawDash(s, baseStyle, &state, opts) drawDash(s, baseStyle, &state, opts)
} else if (ev.Key() == tcell.KeyUp || ev.Rune() == 'k') && state.view == viewTypeQueueDetails { } else if (ev.Key() == tcell.KeyUp || ev.Rune() == 'k') && state.view == viewTypeQueueDetails {
if state.taskTableRowIdx == 0 { if shouldShowGroupTable(&state) {
state.taskTableRowIdx = len(state.tasks) if state.groupTableRowIdx == 0 {
state.groupTableRowIdx = groupPageSize(s)
} else {
state.groupTableRowIdx--
}
} else { } else {
state.taskTableRowIdx-- if state.taskTableRowIdx == 0 {
state.taskTableRowIdx = len(state.tasks)
} else {
state.taskTableRowIdx--
}
} }
drawDash(s, baseStyle, &state, opts) drawDash(s, baseStyle, &state, opts)
} else if ev.Key() == tcell.KeyEnter { } else if ev.Key() == tcell.KeyEnter {
if state.view == viewTypeQueues && state.queueTableRowIdx != 0 { switch state.view {
state.selectedQueue = state.queues[state.queueTableRowIdx-1] case viewTypeQueues:
state.view = viewTypeQueueDetails if state.queueTableRowIdx != 0 {
state.taskState = asynq.TaskStateActive state.selectedQueue = state.queues[state.queueTableRowIdx-1]
state.tasks = nil state.view = viewTypeQueueDetails
state.pageNum = 1 state.taskState = asynq.TaskStateActive
go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState, state.tasks = nil
taskPageSize(s), state.pageNum, tasksCh, errorCh) state.pageNum = 1
ticker.Reset(interval) go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState,
drawDash(s, baseStyle, &state, opts) taskPageSize(s), state.pageNum, tasksCh, errorCh)
ticker.Reset(interval)
drawDash(s, baseStyle, &state, opts)
}
case viewTypeQueueDetails:
if shouldShowGroupTable(&state) && state.groupTableRowIdx != 0 {
state.selectedGroup = state.groups[state.groupTableRowIdx-1]
state.tasks = nil
state.pageNum = 1
go fetchAggregatingTasks(inspector, state.selectedQueue.Queue, state.selectedGroup.Group,
taskPageSize(s), state.pageNum, tasksCh, errorCh)
ticker.Reset(interval)
drawDash(s, baseStyle, &state, opts)
}
} }
} else if ev.Rune() == '?' { } else if ev.Rune() == '?' {
state.prevView = state.view state.prevView = state.view
@@ -196,8 +230,13 @@ func Run(opts Options) {
state.pageNum = 1 state.pageNum = 1
state.taskTableRowIdx = 0 state.taskTableRowIdx = 0
state.tasks = nil state.tasks = nil
go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState, state.selectedGroup = nil
taskPageSize(s), state.pageNum, tasksCh, errorCh) if shouldShowGroupTable(&state) {
go fetchGroups(inspector, state.selectedQueue.Queue, groupsCh, errorCh)
} else {
go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState,
taskPageSize(s), state.pageNum, tasksCh, errorCh)
}
ticker.Reset(interval) ticker.Reset(interval)
drawDash(s, baseStyle, &state, opts) drawDash(s, baseStyle, &state, opts)
} else if (ev.Key() == tcell.KeyLeft || ev.Rune() == 'h') && state.view == viewTypeQueueDetails { } else if (ev.Key() == tcell.KeyLeft || ev.Rune() == 'h') && state.view == viewTypeQueueDetails {
@@ -205,25 +244,50 @@ func Run(opts Options) {
state.pageNum = 1 state.pageNum = 1
state.taskTableRowIdx = 0 state.taskTableRowIdx = 0
state.tasks = nil state.tasks = nil
go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState, state.selectedGroup = nil
taskPageSize(s), state.pageNum, tasksCh, errorCh) if shouldShowGroupTable(&state) {
go fetchGroups(inspector, state.selectedQueue.Queue, groupsCh, errorCh)
} else {
go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState,
taskPageSize(s), state.pageNum, tasksCh, errorCh)
}
ticker.Reset(interval) ticker.Reset(interval)
drawDash(s, baseStyle, &state, opts) drawDash(s, baseStyle, &state, opts)
} else if ev.Rune() == 'n' && state.view == viewTypeQueueDetails { } else if ev.Rune() == 'n' && state.view == viewTypeQueueDetails {
pageSize := taskPageSize(s) if shouldShowGroupTable(&state) {
totalCount := getTaskCount(state.selectedQueue, state.taskState) pageSize := groupPageSize(s)
if (state.pageNum-1)*pageSize+len(state.tasks) < totalCount { total := len(state.groups)
state.pageNum++ start := (state.pageNum - 1) * pageSize
go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState, end := start + pageSize
pageSize, state.pageNum, tasksCh, errorCh) if end <= total {
ticker.Reset(interval) state.pageNum++
drawDash(s, baseStyle, &state, opts)
}
} else {
pageSize := taskPageSize(s)
totalCount := getTaskCount(state.selectedQueue, state.taskState)
if (state.pageNum-1)*pageSize+len(state.tasks) < totalCount {
state.pageNum++
go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState,
pageSize, state.pageNum, tasksCh, errorCh)
ticker.Reset(interval)
}
} }
} else if ev.Rune() == 'p' && state.view == viewTypeQueueDetails { } else if ev.Rune() == 'p' && state.view == viewTypeQueueDetails {
if state.pageNum > 1 { if shouldShowGroupTable(&state) {
state.pageNum-- pageSize := groupPageSize(s)
go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState, start := (state.pageNum - 1) * pageSize
taskPageSize(s), state.pageNum, tasksCh, errorCh) if start > 0 {
ticker.Reset(interval) state.pageNum--
drawDash(s, baseStyle, &state, opts)
}
} else {
if state.pageNum > 1 {
state.pageNum--
go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState,
taskPageSize(s), state.pageNum, tasksCh, errorCh)
ticker.Reset(interval)
}
} }
} }
} }
@@ -234,8 +298,15 @@ func Run(opts Options) {
go fetchQueues(inspector, queuesCh, errorCh, opts) go fetchQueues(inspector, queuesCh, errorCh, opts)
case viewTypeQueueDetails: case viewTypeQueueDetails:
go fetchQueueInfo(inspector, state.selectedQueue.Queue, queueCh, errorCh) go fetchQueueInfo(inspector, state.selectedQueue.Queue, queueCh, errorCh)
go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState, if shouldShowGroupTable(&state) {
taskPageSize(s), state.pageNum, tasksCh, errorCh) go fetchGroups(inspector, state.selectedQueue.Queue, groupsCh, errorCh)
} else if state.taskState == asynq.TaskStateAggregating {
go fetchAggregatingTasks(inspector, state.selectedQueue.Queue, state.selectedGroup.Group,
taskPageSize(s), state.pageNum, tasksCh, errorCh)
} else {
go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState,
taskPageSize(s), state.pageNum, tasksCh, errorCh)
}
case viewTypeRedis: case viewTypeRedis:
go fetchRedisInfo(redisInfoCh, errorCh) go fetchRedisInfo(redisInfoCh, errorCh)
} }
@@ -250,6 +321,11 @@ func Run(opts Options) {
state.err = nil state.err = nil
drawDash(s, baseStyle, &state, opts) drawDash(s, baseStyle, &state, opts)
case groups := <-groupsCh:
state.groups = groups
state.err = nil
drawDash(s, baseStyle, &state, opts)
case tasks := <-tasksCh: case tasks := <-tasksCh:
state.tasks = tasks state.tasks = tasks
state.err = nil state.err = nil

View File

@@ -256,20 +256,31 @@ func drawQueueSummary(d *ScreenDrawer, style tcell.Style, state *State) {
d.Println(byteCount(q.MemoryUsage), style) d.Println(byteCount(q.MemoryUsage), style)
} }
// Returns the max number of groups that can be displayed.
func groupPageSize(s tcell.Screen) int {
_, h := s.Size()
return h - 16 // height - (# of rows used)
}
// Returns the number of tasks to fetch. // Returns the number of tasks to fetch.
func taskPageSize(s tcell.Screen) int { func taskPageSize(s tcell.Screen) int {
_, h := s.Size() _, h := s.Size()
return h - 15 // height - (# of rows used) return h - 15 // height - (# of rows used)
} }
func shouldShowGroupTable(state *State) bool {
return state.taskState == asynq.TaskStateAggregating && state.selectedGroup == nil
}
func drawTaskTable(d *ScreenDrawer, style tcell.Style, state *State) { func drawTaskTable(d *ScreenDrawer, style tcell.Style, state *State) {
if state.taskState == asynq.TaskStateAggregating { if shouldShowGroupTable(state) {
d.Println("TODO: aggregating tasks need group name", style) drawGroupTable(d, style, state)
return return
} }
if len(state.tasks) == 0 { if len(state.tasks) == 0 {
return // print nothing return // print nothing
} }
// TODO: colConfigs should be different for each state
colConfigs := []*columnConfig[*asynq.TaskInfo]{ colConfigs := []*columnConfig[*asynq.TaskInfo]{
{"ID", alignLeft, func(t *asynq.TaskInfo) string { return t.ID }}, {"ID", alignLeft, func(t *asynq.TaskInfo) string { return t.ID }},
{"Type", alignLeft, func(t *asynq.TaskInfo) string { return t.Type }}, {"Type", alignLeft, func(t *asynq.TaskInfo) string { return t.Type }},
@@ -282,6 +293,10 @@ func drawTaskTable(d *ScreenDrawer, style tcell.Style, state *State) {
// Pagination // Pagination
pageSize := taskPageSize(d.Screen()) pageSize := taskPageSize(d.Screen())
totalCount := getTaskCount(state.selectedQueue, state.taskState) totalCount := getTaskCount(state.selectedQueue, state.taskState)
if state.taskState == asynq.TaskStateAggregating {
// aggregating tasks are scoped to each group when shown in the table.
totalCount = state.selectedGroup.Size
}
if pageSize < totalCount { if pageSize < totalCount {
start := (state.pageNum-1)*pageSize + 1 start := (state.pageNum-1)*pageSize + 1
end := start + len(state.tasks) - 1 end := start + len(state.tasks) - 1
@@ -303,6 +318,47 @@ func isNextTaskPageAvailable(s tcell.Screen, state *State) bool {
return end < totalCount return end < totalCount
} }
func drawGroupTable(d *ScreenDrawer, style tcell.Style, state *State) {
if len(state.groups) == 0 {
return // print nothing
}
d.Println("<<< Select group >>>", style)
colConfigs := []*columnConfig[*asynq.GroupInfo]{
{"Name", alignLeft, func(g *asynq.GroupInfo) string { return g.Group }},
{"Size", alignRight, func(g *asynq.GroupInfo) string { return strconv.Itoa(g.Size) }},
}
// pagination
pageSize := groupPageSize(d.Screen())
total := len(state.groups)
start := (state.pageNum - 1) * pageSize
end := min(start+pageSize, total)
drawTable(d, style, colConfigs, state.groups[start:end], state.groupTableRowIdx-1)
footerStyle := style.Foreground(tcell.ColorLightGray)
if pageSize < total {
d.Print(fmt.Sprintf("Showing %d-%d out of %d", start+1, end, total), footerStyle)
if end < total {
d.Print(" n=NextPage", footerStyle)
}
if start > 0 {
d.Print(" p=PrevPage", footerStyle)
}
}
d.FillLine(' ', footerStyle)
}
type number interface {
int | int64 | float64
}
// min returns the smaller of x and y. if x==y, returns x
func min[V number](x, y V) V {
if x > y {
return y
}
return x
}
// Define the order of states to show // Define the order of states to show
var taskStates = []asynq.TaskState{ var taskStates = []asynq.TaskState{
asynq.TaskStateActive, asynq.TaskStateActive,

View File

@@ -56,6 +56,25 @@ func fetchRedisInfo(redisInfoCh chan<- *redisInfo, errorCh chan<- error) {
} }
} }
func fetchGroups(i *asynq.Inspector, qname string, groupsCh chan<- []*asynq.GroupInfo, errorCh chan<- error) {
groups, err := i.Groups(qname)
if err != nil {
errorCh <- err
return
}
groupsCh <- groups
}
func fetchAggregatingTasks(i *asynq.Inspector, qname, group string, pageSize, pageNum int,
tasksCh chan<- []*asynq.TaskInfo, errorCh chan<- error) {
tasks, err := i.ListAggregatingTasks(qname, group, asynq.PageSize(pageSize), asynq.Page(pageNum))
if err != nil {
errorCh <- err
return
}
tasksCh <- tasks
}
func fetchTasks(i *asynq.Inspector, qname string, taskState asynq.TaskState, pageSize, pageNum int, func fetchTasks(i *asynq.Inspector, qname string, taskState asynq.TaskState, pageSize, pageNum int,
tasksCh chan<- []*asynq.TaskInfo, errorCh chan<- error) { tasksCh chan<- []*asynq.TaskInfo, errorCh chan<- error) {
var ( var (