Add completed state

This commit is contained in:
Ken Hibino 2021-11-06 15:23:10 -07:00 committed by GitHub
parent ddb1798ce8
commit 741a3c59fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1234 additions and 201 deletions

3
.gitignore vendored
View File

@ -23,8 +23,9 @@ package-json.lock
# Output of the go coverage tool, specifically when used with LiteIDE # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
# main binary # binaries
asynqmon asynqmon
api
dist/ dist/
# Editor configs # Editor configs

View File

@ -1,10 +1,15 @@
.PHONY: assets build docker .PHONY: api assets build docker
NODE_PATH ?= $(PWD)/ui/node_modules NODE_PATH ?= $(PWD)/ui/node_modules
assets: assets:
@if [ ! -d "$(NODE_PATH)" ]; then cd ./ui && yarn install --modules-folder $(NODE_PATH); fi @if [ ! -d "$(NODE_PATH)" ]; then cd ./ui && yarn install --modules-folder $(NODE_PATH); fi
cd ./ui && yarn build --modules-folder $(NODE_PATH) cd ./ui && yarn build --modules-folder $(NODE_PATH)
# This target skips the overhead of building UI assets.
# Intended to be used during development.
api:
go build -o api ./cmd/asynqmon
# Build a release binary. # Build a release binary.
build: assets build: assets
go build -o asynqmon ./cmd/asynqmon go build -o asynqmon ./cmd/asynqmon

View File

@ -150,7 +150,8 @@ func main() {
RedisConnOpt: asynq.RedisClientOpt{Addr: ":6379"}, RedisConnOpt: asynq.RedisClientOpt{Addr: ":6379"},
}) })
http.Handle(h.RootPath(), h) // Note: We need the tailing slash when using net/http.ServeMux.
http.Handle(h.RootPath()+"/", h)
// Go to http://localhost:8080/monitoring to see asynqmon homepage. // Go to http://localhost:8080/monitoring to see asynqmon homepage.
log.Fatal(http.ListenAndServe(":8080", nil)) log.Fatal(http.ListenAndServe(":8080", nil))

View File

@ -27,6 +27,7 @@ var (
flagRedisInsecureTLS bool flagRedisInsecureTLS bool
flagRedisClusterNodes string flagRedisClusterNodes string
flagMaxPayloadLength int flagMaxPayloadLength int
flagMaxResultLength int
) )
func init() { func init() {
@ -39,6 +40,7 @@ func init() {
flag.BoolVar(&flagRedisInsecureTLS, "redis-insecure-tls", false, "disable TLS certificate host checks") flag.BoolVar(&flagRedisInsecureTLS, "redis-insecure-tls", false, "disable TLS certificate host checks")
flag.StringVar(&flagRedisClusterNodes, "redis-cluster-nodes", "", "comma separated list of host:port addresses of cluster nodes") flag.StringVar(&flagRedisClusterNodes, "redis-cluster-nodes", "", "comma separated list of host:port addresses of cluster nodes")
flag.IntVar(&flagMaxPayloadLength, "max-payload-length", 200, "maximum number of utf8 characters printed in the payload cell in the Web UI") flag.IntVar(&flagMaxPayloadLength, "max-payload-length", 200, "maximum number of utf8 characters printed in the payload cell in the Web UI")
flag.IntVar(&flagMaxResultLength, "max-result-length", 200, "maximum number of utf8 characters printed in the result cell in the Web UI")
} }
// TODO: Write test and refactor this code. // TODO: Write test and refactor this code.
@ -102,6 +104,7 @@ func main() {
h := asynqmon.New(asynqmon.Options{ h := asynqmon.New(asynqmon.Options{
RedisConnOpt: redisConnOpt, RedisConnOpt: redisConnOpt,
PayloadFormatter: asynqmon.PayloadFormatterFunc(formatPayload), PayloadFormatter: asynqmon.PayloadFormatterFunc(formatPayload),
ResultFormatter: asynqmon.ResultFormatterFunc(formatResult),
}) })
defer h.Close() defer h.Close()
@ -125,6 +128,11 @@ func formatPayload(taskType string, payload []byte) string {
return truncate(payloadStr, flagMaxPayloadLength) return truncate(payloadStr, flagMaxPayloadLength)
} }
func formatResult(taskType string, result []byte) string {
resultStr := asynqmon.DefaultResultFormatter.FormatResult(taskType, result)
return truncate(resultStr, flagMaxResultLength)
}
// truncates string s to limit length (in utf8). // truncates string s to limit length (in utf8).
func truncate(s string, limit int) string { func truncate(s string, limit int) string {
i := 0 i := 0

View File

@ -14,7 +14,7 @@ import (
// - conversion function from an external type to an internal type // - conversion function from an external type to an internal type
// **************************************************************************** // ****************************************************************************
// PayloadFormatter is used to convert payload bytes to string shown in the UI. // PayloadFormatter is used to convert payload bytes to a string shown in the UI.
type PayloadFormatter interface { type PayloadFormatter interface {
// FormatPayload takes the task's typename and payload and returns a string representation of the payload. // FormatPayload takes the task's typename and payload and returns a string representation of the payload.
FormatPayload(taskType string, payload []byte) string FormatPayload(taskType string, payload []byte) string
@ -22,11 +22,22 @@ type PayloadFormatter interface {
type PayloadFormatterFunc func(string, []byte) string type PayloadFormatterFunc func(string, []byte) string
// FormatPayload returns a string representation of the payload of the given taskType.
func (f PayloadFormatterFunc) FormatPayload(taskType string, payload []byte) string { func (f PayloadFormatterFunc) FormatPayload(taskType string, payload []byte) string {
return f(taskType, payload) return f(taskType, payload)
} }
// ResultFormatter is used to convert result bytes to a string shown in the UI.
type ResultFormatter interface {
// FormatResult takes the task's typename and result and returns a string representation of the result.
FormatResult(taskType string, result []byte) string
}
type ResultFormatterFunc func(string, []byte) string
func (f ResultFormatterFunc) FormatResult(taskType string, result []byte) string {
return f(taskType, result)
}
// DefaultPayloadFormatter is the PayloadFormater used by default. // DefaultPayloadFormatter is the PayloadFormater used by default.
// It prints the given payload bytes as is if the bytes are printable, otherwise it prints a message to indicate // It prints the given payload bytes as is if the bytes are printable, otherwise it prints a message to indicate
// that the bytes are not printable. // that the bytes are not printable.
@ -37,6 +48,16 @@ var DefaultPayloadFormatter = PayloadFormatterFunc(func(_ string, payload []byte
return string(payload) return string(payload)
}) })
// DefaultResultFormatter is the ResultFormatter used by default.
// It prints the given result bytes as is if the bytes are printable, otherwise it prints a message to indicate
// that the bytes are not printable.
var DefaultResultFormatter = ResultFormatterFunc(func(_ string, result []byte) string {
if !isPrintable(result) {
return "non-printable bytes"
}
return string(result)
})
// isPrintable reports whether the given data is comprised of all printable runes. // isPrintable reports whether the given data is comprised of all printable runes.
func isPrintable(data []byte) bool { func isPrintable(data []byte) bool {
if !utf8.Valid(data) { if !utf8.Valid(data) {
@ -67,6 +88,7 @@ type queueStateSnapshot struct {
Scheduled int `json:"scheduled"` Scheduled int `json:"scheduled"`
Retry int `json:"retry"` Retry int `json:"retry"`
Archived int `json:"archived"` Archived int `json:"archived"`
Completed int `json:"completed"`
// Total number of tasks processed during the given date. // Total number of tasks processed during the given date.
// The number includes both succeeded and failed tasks. // The number includes both succeeded and failed tasks.
@ -91,6 +113,7 @@ func toQueueStateSnapshot(s *asynq.QueueInfo) *queueStateSnapshot {
Scheduled: s.Scheduled, Scheduled: s.Scheduled,
Retry: s.Retry, Retry: s.Retry,
Archived: s.Archived, Archived: s.Archived,
Completed: s.Completed,
Processed: s.Processed, Processed: s.Processed,
Succeeded: s.Processed - s.Failed, Succeeded: s.Processed - s.Failed,
Failed: s.Failed, Failed: s.Failed,
@ -152,6 +175,22 @@ type taskInfo struct {
// NextProcessAt is the time the task is scheduled to be processed in RFC3339 format. // NextProcessAt is the time the task is scheduled to be processed in RFC3339 format.
// If not applicable, empty string. // If not applicable, empty string.
NextProcessAt string `json:"next_process_at"` NextProcessAt string `json:"next_process_at"`
// CompletedAt is the time the task was successfully processed in RFC3339 format.
// If not applicable, empty string.
CompletedAt string `json:"completed_at"`
// Result is the result data associated with the task.
Result string `json:"result"`
// TTL is the number of seconds the task has left to be retained in the queue.
// This is calculated by (CompletedAt + ResultTTL) - Now.
TTL int64 `json:"ttl_seconds"`
}
// taskTTL calculates TTL for the given task.
func taskTTL(task *asynq.TaskInfo) time.Duration {
if task.State != asynq.TaskStateCompleted {
return 0 // N/A
}
return task.CompletedAt.Add(task.Retention).Sub(time.Now())
} }
// formatTimeInRFC3339 formats t in RFC3339 if the value is non-zero. // formatTimeInRFC3339 formats t in RFC3339 if the value is non-zero.
@ -163,7 +202,7 @@ func formatTimeInRFC3339(t time.Time) string {
return t.Format(time.RFC3339) return t.Format(time.RFC3339)
} }
func toTaskInfo(info *asynq.TaskInfo, pf PayloadFormatter) *taskInfo { func toTaskInfo(info *asynq.TaskInfo, pf PayloadFormatter, rf ResultFormatter) *taskInfo {
return &taskInfo{ return &taskInfo{
ID: info.ID, ID: info.ID,
Queue: info.Queue, Queue: info.Queue,
@ -177,6 +216,9 @@ func toTaskInfo(info *asynq.TaskInfo, pf PayloadFormatter) *taskInfo {
Timeout: int(info.Timeout.Seconds()), Timeout: int(info.Timeout.Seconds()),
Deadline: formatTimeInRFC3339(info.Deadline), Deadline: formatTimeInRFC3339(info.Deadline),
NextProcessAt: formatTimeInRFC3339(info.NextProcessAt), NextProcessAt: formatTimeInRFC3339(info.NextProcessAt),
CompletedAt: formatTimeInRFC3339(info.CompletedAt),
Result: rf.FormatResult("", info.Result),
TTL: int64(taskTTL(info).Seconds()),
} }
} }
@ -343,6 +385,40 @@ func toArchivedTasks(in []*asynq.TaskInfo, pf PayloadFormatter) []*archivedTask
return out return out
} }
type completedTask struct {
*baseTask
CompletedAt time.Time `json:"completed_at"`
Result string `json:"result"`
// Number of seconds left for retention (i.e. (CompletedAt + ResultTTL) - Now)
TTL int64 `json:"ttl_seconds"`
}
func toCompletedTask(ti *asynq.TaskInfo, pf PayloadFormatter, rf ResultFormatter) *completedTask {
base := &baseTask{
ID: ti.ID,
Type: ti.Type,
Payload: pf.FormatPayload(ti.Type, ti.Payload),
Queue: ti.Queue,
MaxRetry: ti.MaxRetry,
Retried: ti.Retried,
LastError: ti.LastErr,
}
return &completedTask{
baseTask: base,
CompletedAt: ti.CompletedAt,
TTL: int64(taskTTL(ti).Seconds()),
Result: rf.FormatResult(ti.Type, ti.Result),
}
}
func toCompletedTasks(in []*asynq.TaskInfo, pf PayloadFormatter, rf ResultFormatter) []*completedTask {
out := make([]*completedTask, len(in))
for i, ti := range in {
out[i] = toCompletedTask(ti, pf, rf)
}
return out
}
type schedulerEntry struct { type schedulerEntry struct {
ID string `json:"id"` ID string `json:"id"`
Spec string `json:"spec"` Spec string `json:"spec"`

3
go.mod
View File

@ -5,6 +5,7 @@ go 1.16
require ( require (
github.com/go-redis/redis/v8 v8.11.3 github.com/go-redis/redis/v8 v8.11.3
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/hibiken/asynq v0.18.6 github.com/hibiken/asynq v0.19.0
github.com/rs/cors v1.7.0 github.com/rs/cors v1.7.0
) )

2
go.sum
View File

@ -55,6 +55,8 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hibiken/asynq v0.18.6 h1:pBjtGh2QhDe1+/0yaSc56ANpdQ77BQgVfMIrj+NJrUM= github.com/hibiken/asynq v0.18.6 h1:pBjtGh2QhDe1+/0yaSc56ANpdQ77BQgVfMIrj+NJrUM=
github.com/hibiken/asynq v0.18.6/go.mod h1:tyc63ojaW8SJ5SBm8mvI4DDONsguP5HE85EEl4Qr5Ig= github.com/hibiken/asynq v0.18.6/go.mod h1:tyc63ojaW8SJ5SBm8mvI4DDONsguP5HE85EEl4Qr5Ig=
github.com/hibiken/asynq v0.19.0 h1:AoJhoivymyFhF92ZAmVzxd7jr0RM264HdgkbjPc+x+M=
github.com/hibiken/asynq v0.19.0/go.mod h1:tyc63ojaW8SJ5SBm8mvI4DDONsguP5HE85EEl4Qr5Ig=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=

View File

@ -29,6 +29,11 @@ type Options struct {
// //
// This field is optional. // This field is optional.
PayloadFormatter PayloadFormatter PayloadFormatter PayloadFormatter
// ResultFormatter is used to convert result bytes to string shown in the UI.
//
// This field is optional.
ResultFormatter ResultFormatter
} }
// HTTPHandler is a http.Handler for asynqmon application. // HTTPHandler is a http.Handler for asynqmon application.
@ -78,8 +83,9 @@ func (h *HTTPHandler) Close() error {
} }
// RootPath returns the root URL path used for asynqmon application. // RootPath returns the root URL path used for asynqmon application.
// Returned path string does not have the trailing slash.
func (h *HTTPHandler) RootPath() string { func (h *HTTPHandler) RootPath() string {
return h.rootPath + "/" return h.rootPath
} }
//go:embed ui/build/* //go:embed ui/build/*
@ -88,9 +94,14 @@ var staticContents embed.FS
func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspector) *mux.Router { func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspector) *mux.Router {
router := mux.NewRouter().PathPrefix(opts.RootPath).Subrouter() router := mux.NewRouter().PathPrefix(opts.RootPath).Subrouter()
var pf PayloadFormatter = DefaultPayloadFormatter var payloadFmt PayloadFormatter = DefaultPayloadFormatter
if opts.PayloadFormatter != nil { if opts.PayloadFormatter != nil {
pf = opts.PayloadFormatter payloadFmt = opts.PayloadFormatter
}
var resultFmt ResultFormatter = DefaultResultFormatter
if opts.ResultFormatter != nil {
resultFmt = opts.ResultFormatter
} }
api := router.PathPrefix("/api").Subrouter() api := router.PathPrefix("/api").Subrouter()
@ -105,12 +116,12 @@ func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspecto
api.HandleFunc("/queue_stats", newListQueueStatsHandlerFunc(inspector)).Methods("GET") api.HandleFunc("/queue_stats", newListQueueStatsHandlerFunc(inspector)).Methods("GET")
// Task endpoints. // Task endpoints.
api.HandleFunc("/queues/{qname}/active_tasks", newListActiveTasksHandlerFunc(inspector, pf)).Methods("GET") api.HandleFunc("/queues/{qname}/active_tasks", newListActiveTasksHandlerFunc(inspector, payloadFmt)).Methods("GET")
api.HandleFunc("/queues/{qname}/active_tasks/{task_id}:cancel", newCancelActiveTaskHandlerFunc(inspector)).Methods("POST") api.HandleFunc("/queues/{qname}/active_tasks/{task_id}:cancel", newCancelActiveTaskHandlerFunc(inspector)).Methods("POST")
api.HandleFunc("/queues/{qname}/active_tasks:cancel_all", newCancelAllActiveTasksHandlerFunc(inspector)).Methods("POST") api.HandleFunc("/queues/{qname}/active_tasks:cancel_all", newCancelAllActiveTasksHandlerFunc(inspector)).Methods("POST")
api.HandleFunc("/queues/{qname}/active_tasks:batch_cancel", newBatchCancelActiveTasksHandlerFunc(inspector)).Methods("POST") api.HandleFunc("/queues/{qname}/active_tasks:batch_cancel", newBatchCancelActiveTasksHandlerFunc(inspector)).Methods("POST")
api.HandleFunc("/queues/{qname}/pending_tasks", newListPendingTasksHandlerFunc(inspector, pf)).Methods("GET") api.HandleFunc("/queues/{qname}/pending_tasks", newListPendingTasksHandlerFunc(inspector, payloadFmt)).Methods("GET")
api.HandleFunc("/queues/{qname}/pending_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE") api.HandleFunc("/queues/{qname}/pending_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE")
api.HandleFunc("/queues/{qname}/pending_tasks:delete_all", newDeleteAllPendingTasksHandlerFunc(inspector)).Methods("DELETE") api.HandleFunc("/queues/{qname}/pending_tasks:delete_all", newDeleteAllPendingTasksHandlerFunc(inspector)).Methods("DELETE")
api.HandleFunc("/queues/{qname}/pending_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST") api.HandleFunc("/queues/{qname}/pending_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST")
@ -118,7 +129,7 @@ func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspecto
api.HandleFunc("/queues/{qname}/pending_tasks:archive_all", newArchiveAllPendingTasksHandlerFunc(inspector)).Methods("POST") api.HandleFunc("/queues/{qname}/pending_tasks:archive_all", newArchiveAllPendingTasksHandlerFunc(inspector)).Methods("POST")
api.HandleFunc("/queues/{qname}/pending_tasks:batch_archive", newBatchArchiveTasksHandlerFunc(inspector)).Methods("POST") api.HandleFunc("/queues/{qname}/pending_tasks:batch_archive", newBatchArchiveTasksHandlerFunc(inspector)).Methods("POST")
api.HandleFunc("/queues/{qname}/scheduled_tasks", newListScheduledTasksHandlerFunc(inspector, pf)).Methods("GET") api.HandleFunc("/queues/{qname}/scheduled_tasks", newListScheduledTasksHandlerFunc(inspector, payloadFmt)).Methods("GET")
api.HandleFunc("/queues/{qname}/scheduled_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE") api.HandleFunc("/queues/{qname}/scheduled_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE")
api.HandleFunc("/queues/{qname}/scheduled_tasks:delete_all", newDeleteAllScheduledTasksHandlerFunc(inspector)).Methods("DELETE") api.HandleFunc("/queues/{qname}/scheduled_tasks:delete_all", newDeleteAllScheduledTasksHandlerFunc(inspector)).Methods("DELETE")
api.HandleFunc("/queues/{qname}/scheduled_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST") api.HandleFunc("/queues/{qname}/scheduled_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST")
@ -129,7 +140,7 @@ func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspecto
api.HandleFunc("/queues/{qname}/scheduled_tasks:archive_all", newArchiveAllScheduledTasksHandlerFunc(inspector)).Methods("POST") api.HandleFunc("/queues/{qname}/scheduled_tasks:archive_all", newArchiveAllScheduledTasksHandlerFunc(inspector)).Methods("POST")
api.HandleFunc("/queues/{qname}/scheduled_tasks:batch_archive", newBatchArchiveTasksHandlerFunc(inspector)).Methods("POST") api.HandleFunc("/queues/{qname}/scheduled_tasks:batch_archive", newBatchArchiveTasksHandlerFunc(inspector)).Methods("POST")
api.HandleFunc("/queues/{qname}/retry_tasks", newListRetryTasksHandlerFunc(inspector, pf)).Methods("GET") api.HandleFunc("/queues/{qname}/retry_tasks", newListRetryTasksHandlerFunc(inspector, payloadFmt)).Methods("GET")
api.HandleFunc("/queues/{qname}/retry_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE") api.HandleFunc("/queues/{qname}/retry_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE")
api.HandleFunc("/queues/{qname}/retry_tasks:delete_all", newDeleteAllRetryTasksHandlerFunc(inspector)).Methods("DELETE") api.HandleFunc("/queues/{qname}/retry_tasks:delete_all", newDeleteAllRetryTasksHandlerFunc(inspector)).Methods("DELETE")
api.HandleFunc("/queues/{qname}/retry_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST") api.HandleFunc("/queues/{qname}/retry_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST")
@ -140,7 +151,7 @@ func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspecto
api.HandleFunc("/queues/{qname}/retry_tasks:archive_all", newArchiveAllRetryTasksHandlerFunc(inspector)).Methods("POST") api.HandleFunc("/queues/{qname}/retry_tasks:archive_all", newArchiveAllRetryTasksHandlerFunc(inspector)).Methods("POST")
api.HandleFunc("/queues/{qname}/retry_tasks:batch_archive", newBatchArchiveTasksHandlerFunc(inspector)).Methods("POST") api.HandleFunc("/queues/{qname}/retry_tasks:batch_archive", newBatchArchiveTasksHandlerFunc(inspector)).Methods("POST")
api.HandleFunc("/queues/{qname}/archived_tasks", newListArchivedTasksHandlerFunc(inspector, pf)).Methods("GET") api.HandleFunc("/queues/{qname}/archived_tasks", newListArchivedTasksHandlerFunc(inspector, payloadFmt)).Methods("GET")
api.HandleFunc("/queues/{qname}/archived_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE") api.HandleFunc("/queues/{qname}/archived_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE")
api.HandleFunc("/queues/{qname}/archived_tasks:delete_all", newDeleteAllArchivedTasksHandlerFunc(inspector)).Methods("DELETE") api.HandleFunc("/queues/{qname}/archived_tasks:delete_all", newDeleteAllArchivedTasksHandlerFunc(inspector)).Methods("DELETE")
api.HandleFunc("/queues/{qname}/archived_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST") api.HandleFunc("/queues/{qname}/archived_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST")
@ -148,13 +159,18 @@ func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspecto
api.HandleFunc("/queues/{qname}/archived_tasks:run_all", newRunAllArchivedTasksHandlerFunc(inspector)).Methods("POST") api.HandleFunc("/queues/{qname}/archived_tasks:run_all", newRunAllArchivedTasksHandlerFunc(inspector)).Methods("POST")
api.HandleFunc("/queues/{qname}/archived_tasks:batch_run", newBatchRunTasksHandlerFunc(inspector)).Methods("POST") api.HandleFunc("/queues/{qname}/archived_tasks:batch_run", newBatchRunTasksHandlerFunc(inspector)).Methods("POST")
api.HandleFunc("/queues/{qname}/tasks/{task_id}", newGetTaskHandlerFunc(inspector, pf)).Methods("GET") api.HandleFunc("/queues/{qname}/completed_tasks", newListCompletedTasksHandlerFunc(inspector, payloadFmt, resultFmt)).Methods("GET")
api.HandleFunc("/queues/{qname}/completed_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE")
api.HandleFunc("/queues/{qname}/completed_tasks:delete_all", newDeleteAllCompletedTasksHandlerFunc(inspector)).Methods("DELETE")
api.HandleFunc("/queues/{qname}/completed_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST")
api.HandleFunc("/queues/{qname}/tasks/{task_id}", newGetTaskHandlerFunc(inspector, payloadFmt, resultFmt)).Methods("GET")
// Servers endpoints. // Servers endpoints.
api.HandleFunc("/servers", newListServersHandlerFunc(inspector, pf)).Methods("GET") api.HandleFunc("/servers", newListServersHandlerFunc(inspector, payloadFmt)).Methods("GET")
// Scheduler Entry endpoints. // Scheduler Entry endpoints.
api.HandleFunc("/scheduler_entries", newListSchedulerEntriesHandlerFunc(inspector, pf)).Methods("GET") api.HandleFunc("/scheduler_entries", newListSchedulerEntriesHandlerFunc(inspector, payloadFmt)).Methods("GET")
api.HandleFunc("/scheduler_entries/{entry_id}/enqueue_events", newListSchedulerEnqueueEventsHandlerFunc(inspector)).Methods("GET") api.HandleFunc("/scheduler_entries/{entry_id}/enqueue_events", newListSchedulerEnqueueEventsHandlerFunc(inspector)).Methods("GET")
// Redis info endpoint. // Redis info endpoint.

View File

@ -280,6 +280,36 @@ func newListArchivedTasksHandlerFunc(inspector *asynq.Inspector, pf PayloadForma
} }
} }
func newListCompletedTasksHandlerFunc(inspector *asynq.Inspector, pf PayloadFormatter, rf ResultFormatter) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
qname := vars["qname"]
pageSize, pageNum := getPageOptions(r)
tasks, err := inspector.ListCompletedTasks(qname, asynq.PageSize(pageSize), asynq.Page(pageNum))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
qinfo, err := inspector.GetQueueInfo(qname)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
payload := make(map[string]interface{})
if len(tasks) == 0 {
// avoid nil for the tasks field in json output.
payload["tasks"] = make([]*completedTask, 0)
} else {
payload["tasks"] = toCompletedTasks(tasks, pf, rf)
}
payload["stats"] = toQueueStateSnapshot(qinfo)
if err := json.NewEncoder(w).Encode(payload); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
func newDeleteTaskHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc { func newDeleteTaskHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
@ -400,6 +430,22 @@ func newDeleteAllArchivedTasksHandlerFunc(inspector *asynq.Inspector) http.Handl
} }
} }
func newDeleteAllCompletedTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
qname := mux.Vars(r)["qname"]
n, err := inspector.DeleteAllCompletedTasks(qname)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp := deleteAllTasksResponse{n}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
func newRunAllScheduledTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc { func newRunAllScheduledTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
qname := mux.Vars(r)["qname"] qname := mux.Vars(r)["qname"]
@ -627,7 +673,7 @@ func getPageOptions(r *http.Request) (pageSize, pageNum int) {
return pageSize, pageNum return pageSize, pageNum
} }
func newGetTaskHandlerFunc(inspector *asynq.Inspector, pf PayloadFormatter) http.HandlerFunc { func newGetTaskHandlerFunc(inspector *asynq.Inspector, pf PayloadFormatter, rf ResultFormatter) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
qname, taskid := vars["qname"], vars["task_id"] qname, taskid := vars["qname"], vars["task_id"]
@ -650,7 +696,7 @@ func newGetTaskHandlerFunc(inspector *asynq.Inspector, pf PayloadFormatter) http
return return
} }
if err := json.NewEncoder(w).Encode(toTaskInfo(info, pf)); err != nil { if err := json.NewEncoder(w).Encode(toTaskInfo(info, pf, rf)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }

View File

@ -1,17 +1,17 @@
{ {
"files": { "files": {
"main.js": "/[[.RootPath]]/static/js/main.dec9d0fd.chunk.js", "main.js": "/[[.RootPath]]/static/js/main.525ff6d9.chunk.js",
"main.js.map": "/[[.RootPath]]/static/js/main.dec9d0fd.chunk.js.map", "main.js.map": "/[[.RootPath]]/static/js/main.525ff6d9.chunk.js.map",
"runtime-main.js": "/[[.RootPath]]/static/js/runtime-main.9fea6c1a.js", "runtime-main.js": "/[[.RootPath]]/static/js/runtime-main.9fea6c1a.js",
"runtime-main.js.map": "/[[.RootPath]]/static/js/runtime-main.9fea6c1a.js.map", "runtime-main.js.map": "/[[.RootPath]]/static/js/runtime-main.9fea6c1a.js.map",
"static/js/2.3f9a2354.chunk.js": "/[[.RootPath]]/static/js/2.3f9a2354.chunk.js", "static/js/2.260e42b2.chunk.js": "/[[.RootPath]]/static/js/2.260e42b2.chunk.js",
"static/js/2.3f9a2354.chunk.js.map": "/[[.RootPath]]/static/js/2.3f9a2354.chunk.js.map", "static/js/2.260e42b2.chunk.js.map": "/[[.RootPath]]/static/js/2.260e42b2.chunk.js.map",
"index.html": "/[[.RootPath]]/index.html", "index.html": "/[[.RootPath]]/index.html",
"static/js/2.3f9a2354.chunk.js.LICENSE.txt": "/[[.RootPath]]/static/js/2.3f9a2354.chunk.js.LICENSE.txt" "static/js/2.260e42b2.chunk.js.LICENSE.txt": "/[[.RootPath]]/static/js/2.260e42b2.chunk.js.LICENSE.txt"
}, },
"entrypoints": [ "entrypoints": [
"static/js/runtime-main.9fea6c1a.js", "static/js/runtime-main.9fea6c1a.js",
"static/js/2.3f9a2354.chunk.js", "static/js/2.260e42b2.chunk.js",
"static/js/main.dec9d0fd.chunk.js" "static/js/main.525ff6d9.chunk.js"
] ]
} }

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" type="image/png" href="/[[.RootPath]]/favicon.ico"/><link rel="icon" type="image/png" sizes="32x32" href="/[[.RootPath]]/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="16x16" href="/[[.RootPath]]/favicon-16x16.png"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Asynq monitoring web console"/><link rel="apple-touch-icon" sizes="180x180" href="/[[.RootPath]]/apple-touch-icon.png"/><link rel="manifest" href="/[[.RootPath]]/manifest.json"/><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"/><script>window.ROOT_PATH="/[[.RootPath]]"</script><title>Asynq - Monitoring</title></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function t(t){for(var n,i,l=t[0],a=t[1],f=t[2],c=0,s=[];c<l.length;c++)i=l[c],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n]);for(p&&p(t);s.length;)s.shift()();return u.push.apply(u,f||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,l=1;l<r.length;l++){var a=r[l];0!==o[a]&&(n=!1)}n&&(u.splice(t--,1),e=i(i.s=r[0]))}return e}var n={},o={1:0},u=[];function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.m=e,i.c=n,i.d=function(e,t,r){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(r,n,function(t){return e[t]}.bind(null,n));return r},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="/[[.RootPath]]/";var l=this.webpackJsonpui=this.webpackJsonpui||[],a=l.push.bind(l);l.push=t,l=l.slice();for(var f=0;f<l.length;f++)t(l[f]);var p=a;r()}([])</script><script src="/[[.RootPath]]/static/js/2.3f9a2354.chunk.js"></script><script src="/[[.RootPath]]/static/js/main.dec9d0fd.chunk.js"></script></body></html> <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" type="image/png" href="/[[.RootPath]]/favicon.ico"/><link rel="icon" type="image/png" sizes="32x32" href="/[[.RootPath]]/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="16x16" href="/[[.RootPath]]/favicon-16x16.png"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Asynq monitoring web console"/><link rel="apple-touch-icon" sizes="180x180" href="/[[.RootPath]]/apple-touch-icon.png"/><link rel="manifest" href="/[[.RootPath]]/manifest.json"/><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"/><script>window.ROOT_PATH="/[[.RootPath]]"</script><title>Asynq - Monitoring</title></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function t(t){for(var n,i,l=t[0],a=t[1],f=t[2],c=0,s=[];c<l.length;c++)i=l[c],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n]);for(p&&p(t);s.length;)s.shift()();return u.push.apply(u,f||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,l=1;l<r.length;l++){var a=r[l];0!==o[a]&&(n=!1)}n&&(u.splice(t--,1),e=i(i.s=r[0]))}return e}var n={},o={1:0},u=[];function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.m=e,i.c=n,i.d=function(e,t,r){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(r,n,function(t){return e[t]}.bind(null,n));return r},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="/[[.RootPath]]/";var l=this.webpackJsonpui=this.webpackJsonpui||[],a=l.push.bind(l);l.push=t,l=l.slice();for(var f=0;f<l.length;f++)t(l[f]);var p=a;r()}([])</script><script src="/[[.RootPath]]/static/js/2.260e42b2.chunk.js"></script><script src="/[[.RootPath]]/static/js/main.525ff6d9.chunk.js"></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,6 +4,7 @@ import {
batchDeleteArchivedTasks, batchDeleteArchivedTasks,
batchDeleteRetryTasks, batchDeleteRetryTasks,
batchDeleteScheduledTasks, batchDeleteScheduledTasks,
batchDeleteCompletedTasks,
BatchDeleteTasksResponse, BatchDeleteTasksResponse,
batchArchiveRetryTasks, batchArchiveRetryTasks,
batchArchiveScheduledTasks, batchArchiveScheduledTasks,
@ -17,23 +18,22 @@ import {
deleteAllArchivedTasks, deleteAllArchivedTasks,
deleteAllRetryTasks, deleteAllRetryTasks,
deleteAllScheduledTasks, deleteAllScheduledTasks,
deleteAllCompletedTasks,
deleteArchivedTask, deleteArchivedTask,
deleteRetryTask, deleteRetryTask,
deleteScheduledTask, deleteScheduledTask,
deleteCompletedTask,
archiveAllRetryTasks, archiveAllRetryTasks,
archiveAllScheduledTasks, archiveAllScheduledTasks,
archiveRetryTask, archiveRetryTask,
archiveScheduledTask, archiveScheduledTask,
listActiveTasks, listActiveTasks,
ListActiveTasksResponse,
listArchivedTasks, listArchivedTasks,
ListArchivedTasksResponse,
listPendingTasks, listPendingTasks,
ListPendingTasksResponse, ListTasksResponse,
listRetryTasks, listRetryTasks,
ListRetryTasksResponse,
listScheduledTasks, listScheduledTasks,
ListScheduledTasksResponse, listCompletedTasks,
PaginationOptions, PaginationOptions,
runAllArchivedTasks, runAllArchivedTasks,
runAllRetryTasks, runAllRetryTasks,
@ -72,6 +72,9 @@ export const LIST_RETRY_TASKS_ERROR = "LIST_RETRY_TASKS_ERROR";
export const LIST_ARCHIVED_TASKS_BEGIN = "LIST_ARCHIVED_TASKS_BEGIN"; export const LIST_ARCHIVED_TASKS_BEGIN = "LIST_ARCHIVED_TASKS_BEGIN";
export const LIST_ARCHIVED_TASKS_SUCCESS = "LIST_ARCHIVED_TASKS_SUCCESS"; export const LIST_ARCHIVED_TASKS_SUCCESS = "LIST_ARCHIVED_TASKS_SUCCESS";
export const LIST_ARCHIVED_TASKS_ERROR = "LIST_ARCHIVED_TASKS_ERROR"; export const LIST_ARCHIVED_TASKS_ERROR = "LIST_ARCHIVED_TASKS_ERROR";
export const LIST_COMPLETED_TASKS_BEGIN = "LIST_COMPLETED_TASKS_BEGIN";
export const LIST_COMPLETED_TASKS_SUCCESS = "LIST_COMPLETED_TASKS_SUCCESS";
export const LIST_COMPLETED_TASKS_ERROR = "LIST_COMPLETED_TASKS_ERROR";
export const CANCEL_ACTIVE_TASK_BEGIN = "CANCEL_ACTIVE_TASK_BEGIN"; export const CANCEL_ACTIVE_TASK_BEGIN = "CANCEL_ACTIVE_TASK_BEGIN";
export const CANCEL_ACTIVE_TASK_SUCCESS = "CANCEL_ACTIVE_TASK_SUCCESS"; export const CANCEL_ACTIVE_TASK_SUCCESS = "CANCEL_ACTIVE_TASK_SUCCESS";
export const CANCEL_ACTIVE_TASK_ERROR = "CANCEL_ACTIVE_TASK_ERROR"; export const CANCEL_ACTIVE_TASK_ERROR = "CANCEL_ACTIVE_TASK_ERROR";
@ -213,6 +216,21 @@ export const DELETE_ALL_ARCHIVED_TASKS_SUCCESS =
"DELETE_ALL_ARCHIVED_TASKS_SUCCESS"; "DELETE_ALL_ARCHIVED_TASKS_SUCCESS";
export const DELETE_ALL_ARCHIVED_TASKS_ERROR = export const DELETE_ALL_ARCHIVED_TASKS_ERROR =
"DELETE_ALL_ARCHIVED_TASKS_ERROR"; "DELETE_ALL_ARCHIVED_TASKS_ERROR";
export const DELETE_COMPLETED_TASK_BEGIN = "DELETE_COMPLETED_TASK_BEGIN";
export const DELETE_COMPLETED_TASK_SUCCESS = "DELETE_COMPLETED_TASK_SUCCESS";
export const DELETE_COMPLETED_TASK_ERROR = "DELETE_COMPLETED_TASK_ERROR";
export const DELETE_ALL_COMPLETED_TASKS_BEGIN =
"DELETE_ALL_COMPLETED_TASKS_BEGIN";
export const DELETE_ALL_COMPLETED_TASKS_SUCCESS =
"DELETE_ALL_COMPLETED_TASKS_SUCCESS";
export const DELETE_ALL_COMPLETED_TASKS_ERROR =
"DELETE_ALL_COMPLETED_TASKS_ERROR";
export const BATCH_DELETE_COMPLETED_TASKS_BEGIN =
"BATCH_DELETE_COMPLETED_TASKS_BEGIN";
export const BATCH_DELETE_COMPLETED_TASKS_SUCCESS =
"BATCH_DELETE_COMPLETED_TASKS_SUCCESS";
export const BATCH_DELETE_COMPLETED_TASKS_ERROR =
"BATCH_DELETE_COMPLETED_TASKS_ERROR";
interface GetTaskInfoBeginAction { interface GetTaskInfoBeginAction {
type: typeof GET_TASK_INFO_BEGIN; type: typeof GET_TASK_INFO_BEGIN;
@ -236,7 +254,7 @@ interface ListActiveTasksBeginAction {
interface ListActiveTasksSuccessAction { interface ListActiveTasksSuccessAction {
type: typeof LIST_ACTIVE_TASKS_SUCCESS; type: typeof LIST_ACTIVE_TASKS_SUCCESS;
queue: string; queue: string;
payload: ListActiveTasksResponse; payload: ListTasksResponse;
} }
interface ListActiveTasksErrorAction { interface ListActiveTasksErrorAction {
@ -253,7 +271,7 @@ interface ListPendingTasksBeginAction {
interface ListPendingTasksSuccessAction { interface ListPendingTasksSuccessAction {
type: typeof LIST_PENDING_TASKS_SUCCESS; type: typeof LIST_PENDING_TASKS_SUCCESS;
queue: string; queue: string;
payload: ListPendingTasksResponse; payload: ListTasksResponse;
} }
interface ListPendingTasksErrorAction { interface ListPendingTasksErrorAction {
@ -270,7 +288,7 @@ interface ListScheduledTasksBeginAction {
interface ListScheduledTasksSuccessAction { interface ListScheduledTasksSuccessAction {
type: typeof LIST_SCHEDULED_TASKS_SUCCESS; type: typeof LIST_SCHEDULED_TASKS_SUCCESS;
queue: string; queue: string;
payload: ListScheduledTasksResponse; payload: ListTasksResponse;
} }
interface ListScheduledTasksErrorAction { interface ListScheduledTasksErrorAction {
@ -287,7 +305,7 @@ interface ListRetryTasksBeginAction {
interface ListRetryTasksSuccessAction { interface ListRetryTasksSuccessAction {
type: typeof LIST_RETRY_TASKS_SUCCESS; type: typeof LIST_RETRY_TASKS_SUCCESS;
queue: string; queue: string;
payload: ListRetryTasksResponse; payload: ListTasksResponse;
} }
interface ListRetryTasksErrorAction { interface ListRetryTasksErrorAction {
@ -304,7 +322,7 @@ interface ListArchivedTasksBeginAction {
interface ListArchivedTasksSuccessAction { interface ListArchivedTasksSuccessAction {
type: typeof LIST_ARCHIVED_TASKS_SUCCESS; type: typeof LIST_ARCHIVED_TASKS_SUCCESS;
queue: string; queue: string;
payload: ListArchivedTasksResponse; payload: ListTasksResponse;
} }
interface ListArchivedTasksErrorAction { interface ListArchivedTasksErrorAction {
@ -313,6 +331,23 @@ interface ListArchivedTasksErrorAction {
error: string; // error description error: string; // error description
} }
interface ListCompletedTasksBeginAction {
type: typeof LIST_COMPLETED_TASKS_BEGIN;
queue: string;
}
interface ListCompletedTasksSuccessAction {
type: typeof LIST_COMPLETED_TASKS_SUCCESS;
queue: string;
payload: ListTasksResponse;
}
interface ListCompletedTasksErrorAction {
type: typeof LIST_COMPLETED_TASKS_ERROR;
queue: string;
error: string; // error description
}
interface CancelActiveTaskBeginAction { interface CancelActiveTaskBeginAction {
type: typeof CANCEL_ACTIVE_TASK_BEGIN; type: typeof CANCEL_ACTIVE_TASK_BEGIN;
queue: string; queue: string;
@ -911,6 +946,61 @@ interface DeleteAllArchivedTasksErrorAction {
error: string; error: string;
} }
interface DeleteCompletedTaskBeginAction {
type: typeof DELETE_COMPLETED_TASK_BEGIN;
queue: string;
taskId: string;
}
interface DeleteCompletedTaskSuccessAction {
type: typeof DELETE_COMPLETED_TASK_SUCCESS;
queue: string;
taskId: string;
}
interface DeleteCompletedTaskErrorAction {
type: typeof DELETE_COMPLETED_TASK_ERROR;
queue: string;
taskId: string;
error: string;
}
interface BatchDeleteCompletedTasksBeginAction {
type: typeof BATCH_DELETE_COMPLETED_TASKS_BEGIN;
queue: string;
taskIds: string[];
}
interface BatchDeleteCompletedTasksSuccessAction {
type: typeof BATCH_DELETE_COMPLETED_TASKS_SUCCESS;
queue: string;
payload: BatchDeleteTasksResponse;
}
interface BatchDeleteCompletedTasksErrorAction {
type: typeof BATCH_DELETE_COMPLETED_TASKS_ERROR;
queue: string;
taskIds: string[];
error: string;
}
interface DeleteAllCompletedTasksBeginAction {
type: typeof DELETE_ALL_COMPLETED_TASKS_BEGIN;
queue: string;
}
interface DeleteAllCompletedTasksSuccessAction {
type: typeof DELETE_ALL_COMPLETED_TASKS_SUCCESS;
queue: string;
deleted: number;
}
interface DeleteAllCompletedTasksErrorAction {
type: typeof DELETE_ALL_COMPLETED_TASKS_ERROR;
queue: string;
error: string;
}
// Union of all tasks related action types. // Union of all tasks related action types.
export type TasksActionTypes = export type TasksActionTypes =
| GetTaskInfoBeginAction | GetTaskInfoBeginAction
@ -931,6 +1021,9 @@ export type TasksActionTypes =
| ListArchivedTasksBeginAction | ListArchivedTasksBeginAction
| ListArchivedTasksSuccessAction | ListArchivedTasksSuccessAction
| ListArchivedTasksErrorAction | ListArchivedTasksErrorAction
| ListCompletedTasksBeginAction
| ListCompletedTasksSuccessAction
| ListCompletedTasksErrorAction
| CancelActiveTaskBeginAction | CancelActiveTaskBeginAction
| CancelActiveTaskSuccessAction | CancelActiveTaskSuccessAction
| CancelActiveTaskErrorAction | CancelActiveTaskErrorAction
@ -1029,7 +1122,16 @@ export type TasksActionTypes =
| RunAllArchivedTasksErrorAction | RunAllArchivedTasksErrorAction
| DeleteAllArchivedTasksBeginAction | DeleteAllArchivedTasksBeginAction
| DeleteAllArchivedTasksSuccessAction | DeleteAllArchivedTasksSuccessAction
| DeleteAllArchivedTasksErrorAction; | DeleteAllArchivedTasksErrorAction
| DeleteCompletedTaskBeginAction
| DeleteCompletedTaskSuccessAction
| DeleteCompletedTaskErrorAction
| BatchDeleteCompletedTasksBeginAction
| BatchDeleteCompletedTasksSuccessAction
| BatchDeleteCompletedTasksErrorAction
| DeleteAllCompletedTasksBeginAction
| DeleteAllCompletedTasksSuccessAction
| DeleteAllCompletedTasksErrorAction;
export function getTaskInfoAsync(qname: string, id: string) { export function getTaskInfoAsync(qname: string, id: string) {
return async (dispatch: Dispatch<TasksActionTypes>) => { return async (dispatch: Dispatch<TasksActionTypes>) => {
@ -1039,15 +1141,15 @@ export function getTaskInfoAsync(qname: string, id: string) {
dispatch({ dispatch({
type: GET_TASK_INFO_SUCCESS, type: GET_TASK_INFO_SUCCESS,
payload: response, payload: response,
}) });
} catch (error) { } catch (error) {
console.error("getTaskInfoAsync: ", toErrorStringWithHttpStatus(error)); console.error("getTaskInfoAsync: ", toErrorStringWithHttpStatus(error));
dispatch({ dispatch({
type: GET_TASK_INFO_ERROR, type: GET_TASK_INFO_ERROR,
error: toErrorString(error), error: toErrorString(error),
}) });
}
} }
};
} }
export function listActiveTasksAsync( export function listActiveTasksAsync(
@ -1185,6 +1287,33 @@ export function listArchivedTasksAsync(
}; };
} }
export function listCompletedTasksAsync(
qname: string,
pageOpts?: PaginationOptions
) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
try {
dispatch({ type: LIST_COMPLETED_TASKS_BEGIN, queue: qname });
const response = await listCompletedTasks(qname, pageOpts);
dispatch({
type: LIST_COMPLETED_TASKS_SUCCESS,
queue: qname,
payload: response,
});
} catch (error) {
console.error(
"listCompletedTasksAsync: ",
toErrorStringWithHttpStatus(error)
);
dispatch({
type: LIST_COMPLETED_TASKS_ERROR,
queue: qname,
error: toErrorString(error),
});
}
};
}
export function cancelActiveTaskAsync(queue: string, taskId: string) { export function cancelActiveTaskAsync(queue: string, taskId: string) {
return async (dispatch: Dispatch<TasksActionTypes>) => { return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: CANCEL_ACTIVE_TASK_BEGIN, queue, taskId }); dispatch({ type: CANCEL_ACTIVE_TASK_BEGIN, queue, taskId });
@ -1395,10 +1524,7 @@ export function deletePendingTaskAsync(queue: string, taskId: string) {
}; };
} }
export function batchDeletePendingTasksAsync( export function batchDeletePendingTasksAsync(queue: string, taskIds: string[]) {
queue: string,
taskIds: string[]
) {
return async (dispatch: Dispatch<TasksActionTypes>) => { return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: BATCH_DELETE_PENDING_TASKS_BEGIN, queue, taskIds }); dispatch({ type: BATCH_DELETE_PENDING_TASKS_BEGIN, queue, taskIds });
try { try {
@ -1938,3 +2064,76 @@ export function runAllArchivedTasksAsync(queue: string) {
} }
}; };
} }
export function deleteCompletedTaskAsync(queue: string, taskId: string) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: DELETE_COMPLETED_TASK_BEGIN, queue, taskId });
try {
await deleteCompletedTask(queue, taskId);
dispatch({ type: DELETE_COMPLETED_TASK_SUCCESS, queue, taskId });
} catch (error) {
console.error(
"deleteCompletedTaskAsync: ",
toErrorStringWithHttpStatus(error)
);
dispatch({
type: DELETE_COMPLETED_TASK_ERROR,
error: toErrorString(error),
queue,
taskId,
});
}
};
}
export function batchDeleteCompletedTasksAsync(
queue: string,
taskIds: string[]
) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: BATCH_DELETE_COMPLETED_TASKS_BEGIN, queue, taskIds });
try {
const response = await batchDeleteCompletedTasks(queue, taskIds);
dispatch({
type: BATCH_DELETE_COMPLETED_TASKS_SUCCESS,
queue: queue,
payload: response,
});
} catch (error) {
console.error(
"batchDeleteCompletedTasksAsync: ",
toErrorStringWithHttpStatus(error)
);
dispatch({
type: BATCH_DELETE_COMPLETED_TASKS_ERROR,
error: toErrorString(error),
queue,
taskIds,
});
}
};
}
export function deleteAllCompletedTasksAsync(queue: string) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: DELETE_ALL_COMPLETED_TASKS_BEGIN, queue });
try {
const response = await deleteAllCompletedTasks(queue);
dispatch({
type: DELETE_ALL_COMPLETED_TASKS_SUCCESS,
deleted: response.deleted,
queue,
});
} catch (error) {
console.error(
"deleteAllCompletedTasksAsync: ",
toErrorStringWithHttpStatus(error)
);
dispatch({
type: DELETE_ALL_COMPLETED_TASKS_ERROR,
error: toErrorString(error),
queue,
});
}
};
}

View File

@ -5,34 +5,16 @@ import queryString from "query-string";
// the static file server. // the static file server.
// In developement, we assume that the API server is listening on port 8080. // In developement, we assume that the API server is listening on port 8080.
const BASE_URL = const BASE_URL =
process.env.NODE_ENV === "production" ? `${window.ROOT_PATH}/api` : `http://localhost:8080${window.ROOT_PATH}/api`; process.env.NODE_ENV === "production"
? `${window.ROOT_PATH}/api`
: `http://localhost:8080${window.ROOT_PATH}/api`;
export interface ListQueuesResponse { export interface ListQueuesResponse {
queues: Queue[]; queues: Queue[];
} }
export interface ListActiveTasksResponse { export interface ListTasksResponse {
tasks: ActiveTask[]; tasks: TaskInfo[];
stats: Queue;
}
export interface ListPendingTasksResponse {
tasks: PendingTask[];
stats: Queue;
}
export interface ListScheduledTasksResponse {
tasks: ScheduledTask[];
stats: Queue;
}
export interface ListRetryTasksResponse {
tasks: RetryTask[];
stats: Queue;
}
export interface ListArchivedTasksResponse {
tasks: ArchivedTask[];
stats: Queue; stats: Queue;
} }
@ -239,6 +221,7 @@ export interface Queue {
scheduled: number; scheduled: number;
retry: number; retry: number;
archived: number; archived: number;
completed: number;
processed: number; processed: number;
failed: number; failed: number;
timestamp: string; timestamp: string;
@ -251,18 +234,13 @@ export interface DailyStat {
failed: number; failed: number;
} }
// BaseTask corresponds to asynq.Task type.
interface BaseTask {
type: string;
payload: string;
}
export interface TaskInfo { export interface TaskInfo {
id: string; id: string;
queue: string; queue: string;
type: string; type: string;
payload: string; payload: string;
state: string; state: string;
start_time: string; // Only applies to task.state == 'active'
max_retry: number; max_retry: number;
retried: number; retried: number;
last_failed_at: string; last_failed_at: string;
@ -270,51 +248,9 @@ export interface TaskInfo {
next_process_at: string; next_process_at: string;
timeout_seconds: number; timeout_seconds: number;
deadline: string; deadline: string;
} completed_at: string;
result: string;
export interface ActiveTask extends BaseTask { ttl_seconds: number;
id: string;
queue: string;
start_time: string;
deadline: string;
max_retry: number;
retried: number;
error_message: string;
}
export interface PendingTask extends BaseTask {
id: string;
queue: string;
max_retry: number;
retried: number;
error_message: string;
}
export interface ScheduledTask extends BaseTask {
id: string;
queue: string;
max_retry: number;
retried: number;
error_message: string;
next_process_at: string;
}
export interface RetryTask extends BaseTask {
id: string;
queue: string;
next_process_at: string;
max_retry: number;
retried: number;
error_message: string;
}
export interface ArchivedTask extends BaseTask {
id: string;
queue: string;
max_retry: number;
retried: number;
last_failed_at: string;
error_message: string;
} }
export interface ServerInfo { export interface ServerInfo {
@ -396,7 +332,10 @@ export async function listQueueStats(): Promise<ListQueueStatsResponse> {
return resp.data; return resp.data;
} }
export async function getTaskInfo(qname: string, id: string): Promise<TaskInfo> { export async function getTaskInfo(
qname: string,
id: string
): Promise<TaskInfo> {
const url = `${BASE_URL}/queues/${qname}/tasks/${id}`; const url = `${BASE_URL}/queues/${qname}/tasks/${id}`;
const resp = await axios({ const resp = await axios({
method: "get", method: "get",
@ -408,7 +347,7 @@ export async function getTaskInfo(qname: string, id: string): Promise<TaskInfo>
export async function listActiveTasks( export async function listActiveTasks(
qname: string, qname: string,
pageOpts?: PaginationOptions pageOpts?: PaginationOptions
): Promise<ListActiveTasksResponse> { ): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/active_tasks`; let url = `${BASE_URL}/queues/${qname}/active_tasks`;
if (pageOpts) { if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`; url += `?${queryString.stringify(pageOpts)}`;
@ -454,7 +393,7 @@ export async function batchCancelActiveTasks(
export async function listPendingTasks( export async function listPendingTasks(
qname: string, qname: string,
pageOpts?: PaginationOptions pageOpts?: PaginationOptions
): Promise<ListPendingTasksResponse> { ): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/pending_tasks`; let url = `${BASE_URL}/queues/${qname}/pending_tasks`;
if (pageOpts) { if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`; url += `?${queryString.stringify(pageOpts)}`;
@ -469,7 +408,7 @@ export async function listPendingTasks(
export async function listScheduledTasks( export async function listScheduledTasks(
qname: string, qname: string,
pageOpts?: PaginationOptions pageOpts?: PaginationOptions
): Promise<ListScheduledTasksResponse> { ): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/scheduled_tasks`; let url = `${BASE_URL}/queues/${qname}/scheduled_tasks`;
if (pageOpts) { if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`; url += `?${queryString.stringify(pageOpts)}`;
@ -484,7 +423,7 @@ export async function listScheduledTasks(
export async function listRetryTasks( export async function listRetryTasks(
qname: string, qname: string,
pageOpts?: PaginationOptions pageOpts?: PaginationOptions
): Promise<ListRetryTasksResponse> { ): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/retry_tasks`; let url = `${BASE_URL}/queues/${qname}/retry_tasks`;
if (pageOpts) { if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`; url += `?${queryString.stringify(pageOpts)}`;
@ -499,7 +438,7 @@ export async function listRetryTasks(
export async function listArchivedTasks( export async function listArchivedTasks(
qname: string, qname: string,
pageOpts?: PaginationOptions pageOpts?: PaginationOptions
): Promise<ListArchivedTasksResponse> { ): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/archived_tasks`; let url = `${BASE_URL}/queues/${qname}/archived_tasks`;
if (pageOpts) { if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`; url += `?${queryString.stringify(pageOpts)}`;
@ -511,6 +450,21 @@ export async function listArchivedTasks(
return resp.data; return resp.data;
} }
export async function listCompletedTasks(
qname: string,
pageOpts?: PaginationOptions
): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/completed_tasks`;
if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`;
}
const resp = await axios({
method: "get",
url,
});
return resp.data;
}
export async function archivePendingTask( export async function archivePendingTask(
qname: string, qname: string,
taskId: string taskId: string
@ -833,6 +787,40 @@ export async function runAllArchivedTasks(qname: string): Promise<void> {
}); });
} }
export async function deleteCompletedTask(
qname: string,
taskId: string
): Promise<void> {
await axios({
method: "delete",
url: `${BASE_URL}/queues/${qname}/completed_tasks/${taskId}`,
});
}
export async function batchDeleteCompletedTasks(
qname: string,
taskIds: string[]
): Promise<BatchDeleteTasksResponse> {
const resp = await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/completed_tasks:batch_delete`,
data: {
task_ids: taskIds,
},
});
return resp.data;
}
export async function deleteAllCompletedTasks(
qname: string
): Promise<DeleteAllTasksResponse> {
const resp = await axios({
method: "delete",
url: `${BASE_URL}/queues/${qname}/completed_tasks:delete_all`,
});
return resp.data;
}
export async function listServers(): Promise<ListServersResponse> { export async function listServers(): Promise<ListServersResponse> {
const resp = await axios({ const resp = await axios({
method: "get", method: "get",

View File

@ -37,7 +37,7 @@ import { taskRowsPerPageChange } from "../actions/settingsActions";
import TableActions from "./TableActions"; import TableActions from "./TableActions";
import { timeAgo, uuidPrefix } from "../utils"; import { timeAgo, uuidPrefix } from "../utils";
import { usePolling } from "../hooks"; import { usePolling } from "../hooks";
import { ArchivedTaskExtended } from "../reducers/tasksReducer"; import { TaskInfoExtended } from "../reducers/tasksReducer";
import { TableColumn } from "../types/table"; import { TableColumn } from "../types/table";
import { taskDetailsPath } from "../paths"; import { taskDetailsPath } from "../paths";
@ -311,7 +311,7 @@ const useRowStyles = makeStyles((theme) => ({
})); }));
interface RowProps { interface RowProps {
task: ArchivedTaskExtended; task: TaskInfoExtended;
isSelected: boolean; isSelected: boolean;
onSelectChange: (checked: boolean) => void; onSelectChange: (checked: boolean) => void;
onRunClick: () => void; onRunClick: () => void;

View File

@ -0,0 +1,376 @@
import React, { useCallback, useState } from "react";
import { useHistory } from "react-router-dom";
import { connect, ConnectedProps } from "react-redux";
import { makeStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import Checkbox from "@material-ui/core/Checkbox";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import Tooltip from "@material-ui/core/Tooltip";
import Paper from "@material-ui/core/Paper";
import IconButton from "@material-ui/core/IconButton";
import DeleteIcon from "@material-ui/icons/Delete";
import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
import TableFooter from "@material-ui/core/TableFooter";
import TablePagination from "@material-ui/core/TablePagination";
import Alert from "@material-ui/lab/Alert";
import AlertTitle from "@material-ui/lab/AlertTitle";
import SyntaxHighlighter from "./SyntaxHighlighter";
import { AppState } from "../store";
import {
listCompletedTasksAsync,
deleteAllCompletedTasksAsync,
deleteCompletedTaskAsync,
batchDeleteCompletedTasksAsync,
} from "../actions/tasksActions";
import TablePaginationActions, {
rowsPerPageOptions,
} from "./TablePaginationActions";
import { taskRowsPerPageChange } from "../actions/settingsActions";
import TableActions from "./TableActions";
import {
durationFromSeconds,
stringifyDuration,
timeAgo,
uuidPrefix,
} from "../utils";
import { usePolling } from "../hooks";
import { TaskInfoExtended } from "../reducers/tasksReducer";
import { TableColumn } from "../types/table";
import { taskDetailsPath } from "../paths";
const useStyles = makeStyles((theme) => ({
table: {
minWidth: 650,
},
stickyHeaderCell: {
background: theme.palette.background.paper,
},
alert: {
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
},
pagination: {
border: "none",
},
}));
function mapStateToProps(state: AppState) {
return {
loading: state.tasks.completedTasks.loading,
error: state.tasks.completedTasks.error,
tasks: state.tasks.completedTasks.data,
batchActionPending: state.tasks.completedTasks.batchActionPending,
allActionPending: state.tasks.completedTasks.allActionPending,
pollInterval: state.settings.pollInterval,
pageSize: state.settings.taskRowsPerPage,
};
}
const mapDispatchToProps = {
listCompletedTasksAsync,
deleteCompletedTaskAsync,
deleteAllCompletedTasksAsync,
batchDeleteCompletedTasksAsync,
taskRowsPerPageChange,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
type ReduxProps = ConnectedProps<typeof connector>;
interface Props {
queue: string; // name of the queue.
totalTaskCount: number; // totoal number of completed tasks.
}
function CompletedTasksTable(props: Props & ReduxProps) {
const { pollInterval, listCompletedTasksAsync, queue, pageSize } = props;
const classes = useStyles();
const [page, setPage] = useState(0);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [activeTaskId, setActiveTaskId] = useState<string>("");
const handlePageChange = (
event: React.MouseEvent<HTMLButtonElement> | null,
newPage: number
) => {
setPage(newPage);
};
const handleRowsPerPageChange = (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
props.taskRowsPerPageChange(parseInt(event.target.value, 10));
setPage(0);
};
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
const newSelected = props.tasks.map((t) => t.id);
setSelectedIds(newSelected);
} else {
setSelectedIds([]);
}
};
const handleDeleteAllClick = () => {
props.deleteAllCompletedTasksAsync(queue);
};
const handleBatchDeleteClick = () => {
props
.batchDeleteCompletedTasksAsync(queue, selectedIds)
.then(() => setSelectedIds([]));
};
const fetchData = useCallback(() => {
const pageOpts = { page: page + 1, size: pageSize };
listCompletedTasksAsync(queue, pageOpts);
}, [page, pageSize, queue, listCompletedTasksAsync]);
usePolling(fetchData, pollInterval);
if (props.error.length > 0) {
return (
<Alert severity="error" className={classes.alert}>
<AlertTitle>Error</AlertTitle>
{props.error}
</Alert>
);
}
if (props.tasks.length === 0) {
return (
<Alert severity="info" className={classes.alert}>
<AlertTitle>Info</AlertTitle>
No completed tasks at this time.
</Alert>
);
}
const columns: TableColumn[] = [
{ key: "id", label: "ID", align: "left" },
{ key: "type", label: "Type", align: "left" },
{ key: "payload", label: "Payload", align: "left" },
{ key: "completed_at", label: "Completed", align: "left" },
{ key: "result", label: "Result", align: "left" },
{ key: "ttl", label: "TTL", align: "left" },
{ key: "actions", label: "Actions", align: "center" },
];
const rowCount = props.tasks.length;
const numSelected = selectedIds.length;
return (
<div>
<TableActions
showIconButtons={numSelected > 0}
iconButtonActions={[
{
tooltip: "Delete",
icon: <DeleteIcon />,
onClick: handleBatchDeleteClick,
disabled: props.batchActionPending,
},
]}
menuItemActions={[
{
label: "Delete All",
onClick: handleDeleteAllClick,
disabled: props.allActionPending,
},
]}
/>
<TableContainer component={Paper}>
<Table
stickyHeader={true}
className={classes.table}
aria-label="archived tasks table"
size="small"
>
<TableHead>
<TableRow>
<TableCell
padding="checkbox"
classes={{ stickyHeader: classes.stickyHeaderCell }}
>
<IconButton>
<Checkbox
indeterminate={numSelected > 0 && numSelected < rowCount}
checked={rowCount > 0 && numSelected === rowCount}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all tasks shown in the table",
}}
/>
</IconButton>
</TableCell>
{columns.map((col) => (
<TableCell
key={col.key}
align={col.align}
classes={{ stickyHeader: classes.stickyHeaderCell }}
>
{col.label}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{props.tasks.map((task) => (
<Row
key={task.id}
task={task}
isSelected={selectedIds.includes(task.id)}
onSelectChange={(checked: boolean) => {
if (checked) {
setSelectedIds(selectedIds.concat(task.id));
} else {
setSelectedIds(selectedIds.filter((id) => id !== task.id));
}
}}
onDeleteClick={() => {
props.deleteCompletedTaskAsync(queue, task.id);
}}
allActionPending={props.allActionPending}
onActionCellEnter={() => setActiveTaskId(task.id)}
onActionCellLeave={() => setActiveTaskId("")}
showActions={activeTaskId === task.id}
/>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={rowsPerPageOptions}
colSpan={columns.length + 1}
count={props.totalTaskCount}
rowsPerPage={pageSize}
page={page}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true,
}}
onPageChange={handlePageChange}
onRowsPerPageChange={handleRowsPerPageChange}
ActionsComponent={TablePaginationActions}
className={classes.pagination}
/>
</TableRow>
</TableFooter>
</Table>
</TableContainer>
</div>
);
}
const useRowStyles = makeStyles((theme) => ({
root: {
cursor: "pointer",
"&:hover": {
boxShadow: theme.shadows[2],
},
"&:hover .MuiTableCell-root": {
borderBottomColor: theme.palette.background.paper,
},
},
actionCell: {
width: "96px",
},
actionButton: {
marginLeft: 3,
marginRight: 3,
},
}));
interface RowProps {
task: TaskInfoExtended;
isSelected: boolean;
onSelectChange: (checked: boolean) => void;
onDeleteClick: () => void;
allActionPending: boolean;
showActions: boolean;
onActionCellEnter: () => void;
onActionCellLeave: () => void;
}
function Row(props: RowProps) {
const { task } = props;
const classes = useRowStyles();
const history = useHistory();
return (
<TableRow
key={task.id}
className={classes.root}
selected={props.isSelected}
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
>
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
<IconButton>
<Checkbox
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
props.onSelectChange(event.target.checked)
}
checked={props.isSelected}
/>
</IconButton>
</TableCell>
<TableCell component="th" scope="row">
{uuidPrefix(task.id)}
</TableCell>
<TableCell>{task.type}</TableCell>
<TableCell>
<SyntaxHighlighter
language="json"
customStyle={{ margin: 0, maxWidth: 400 }}
>
{task.payload}
</SyntaxHighlighter>
</TableCell>
<TableCell>{timeAgo(task.completed_at)}</TableCell>
<TableCell>
<SyntaxHighlighter
language="json"
customStyle={{ margin: 0, maxWidth: 400 }}
>
{task.result}
</SyntaxHighlighter>
</TableCell>
<TableCell>
{task.ttl_seconds > 0
? `${stringifyDuration(durationFromSeconds(task.ttl_seconds))} left`
: `expired`}
</TableCell>
<TableCell
align="center"
className={classes.actionCell}
onMouseEnter={props.onActionCellEnter}
onMouseLeave={props.onActionCellLeave}
onClick={(e) => e.stopPropagation()}
>
{props.showActions ? (
<React.Fragment>
<Tooltip title="Delete">
<IconButton
className={classes.actionButton}
onClick={props.onDeleteClick}
disabled={task.requestPending || props.allActionPending}
size="small"
>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
</React.Fragment>
) : (
<IconButton size="small" onClick={props.onActionCellEnter}>
<MoreHorizIcon fontSize="small" />
</IconButton>
)}
</TableCell>
</TableRow>
);
}
export default connector(CompletedTasksTable);

View File

@ -38,7 +38,7 @@ import { AppState } from "../store";
import { usePolling } from "../hooks"; import { usePolling } from "../hooks";
import { uuidPrefix } from "../utils"; import { uuidPrefix } from "../utils";
import { TableColumn } from "../types/table"; import { TableColumn } from "../types/table";
import { PendingTaskExtended } from "../reducers/tasksReducer"; import { TaskInfoExtended } from "../reducers/tasksReducer";
import { taskDetailsPath } from "../paths"; import { taskDetailsPath } from "../paths";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
@ -313,7 +313,7 @@ const useRowStyles = makeStyles((theme) => ({
})); }));
interface RowProps { interface RowProps {
task: PendingTaskExtended; task: TaskInfoExtended;
isSelected: boolean; isSelected: boolean;
onSelectChange: (checked: boolean) => void; onSelectChange: (checked: boolean) => void;
onDeleteClick: () => void; onDeleteClick: () => void;

View File

@ -23,6 +23,7 @@ interface TaskBreakdown {
scheduled: number; // number of scheduled tasks in the queue. scheduled: number; // number of scheduled tasks in the queue.
retry: number; // number of retry tasks in the queue. retry: number; // number of retry tasks in the queue.
archived: number; // number of archived tasks in the queue. archived: number; // number of archived tasks in the queue.
completed: number; // number of completed tasks in the queue.
} }
function QueueSizeChart(props: Props) { function QueueSizeChart(props: Props) {
@ -55,6 +56,7 @@ function QueueSizeChart(props: Props) {
<Bar dataKey="scheduled" stackId="a" fill="#fdd663" /> <Bar dataKey="scheduled" stackId="a" fill="#fdd663" />
<Bar dataKey="retry" stackId="a" fill="#f666a9" /> <Bar dataKey="retry" stackId="a" fill="#f666a9" />
<Bar dataKey="archived" stackId="a" fill="#ac4776" /> <Bar dataKey="archived" stackId="a" fill="#ac4776" />
<Bar dataKey="completed" stackId="a" fill="#4bb543" />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
); );

View File

@ -41,7 +41,7 @@ import { taskRowsPerPageChange } from "../actions/settingsActions";
import TableActions from "./TableActions"; import TableActions from "./TableActions";
import { durationBefore, uuidPrefix } from "../utils"; import { durationBefore, uuidPrefix } from "../utils";
import { usePolling } from "../hooks"; import { usePolling } from "../hooks";
import { RetryTaskExtended } from "../reducers/tasksReducer"; import { TaskInfoExtended } from "../reducers/tasksReducer";
import { TableColumn } from "../types/table"; import { TableColumn } from "../types/table";
import { taskDetailsPath } from "../paths"; import { taskDetailsPath } from "../paths";
@ -344,7 +344,7 @@ const useRowStyles = makeStyles((theme) => ({
})); }));
interface RowProps { interface RowProps {
task: RetryTaskExtended; task: TaskInfoExtended;
isSelected: boolean; isSelected: boolean;
onSelectChange: (checked: boolean) => void; onSelectChange: (checked: boolean) => void;
onDeleteClick: () => void; onDeleteClick: () => void;

View File

@ -41,7 +41,7 @@ import TablePaginationActions, {
import TableActions from "./TableActions"; import TableActions from "./TableActions";
import { durationBefore, uuidPrefix } from "../utils"; import { durationBefore, uuidPrefix } from "../utils";
import { usePolling } from "../hooks"; import { usePolling } from "../hooks";
import { ScheduledTaskExtended } from "../reducers/tasksReducer"; import { TaskInfoExtended } from "../reducers/tasksReducer";
import { TableColumn } from "../types/table"; import { TableColumn } from "../types/table";
import { taskDetailsPath } from "../paths"; import { taskDetailsPath } from "../paths";
@ -341,7 +341,7 @@ const useRowStyles = makeStyles((theme) => ({
})); }));
interface RowProps { interface RowProps {
task: ScheduledTaskExtended; task: TaskInfoExtended;
isSelected: boolean; isSelected: boolean;
onSelectChange: (checked: boolean) => void; onSelectChange: (checked: boolean) => void;
onRunClick: () => void; onRunClick: () => void;

View File

@ -11,6 +11,7 @@ import PendingTasksTable from "./PendingTasksTable";
import ScheduledTasksTable from "./ScheduledTasksTable"; import ScheduledTasksTable from "./ScheduledTasksTable";
import RetryTasksTable from "./RetryTasksTable"; import RetryTasksTable from "./RetryTasksTable";
import ArchivedTasksTable from "./ArchivedTasksTable"; import ArchivedTasksTable from "./ArchivedTasksTable";
import CompletedTasksTable from "./CompletedTasksTable";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { queueDetailsPath, taskDetailsPath } from "../paths"; import { queueDetailsPath, taskDetailsPath } from "../paths";
import { QueueInfo } from "../reducers/queuesReducer"; import { QueueInfo } from "../reducers/queuesReducer";
@ -56,6 +57,7 @@ function mapStatetoProps(state: AppState, ownProps: Props) {
scheduled: 0, scheduled: 0,
retry: 0, retry: 0,
archived: 0, archived: 0,
completed: 0,
processed: 0, processed: 0,
failed: 0, failed: 0,
timestamp: "n/a", timestamp: "n/a",
@ -104,7 +106,8 @@ const useStyles = makeStyles((theme) => ({
marginLeft: "2px", marginLeft: "2px",
}, },
searchbar: { searchbar: {
marginLeft: theme.spacing(4), paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
}, },
search: { search: {
position: "relative", position: "relative",
@ -147,6 +150,7 @@ function TasksTable(props: Props & ReduxProps) {
{ key: "scheduled", label: "Scheduled", count: currentStats.scheduled }, { key: "scheduled", label: "Scheduled", count: currentStats.scheduled },
{ key: "retry", label: "Retry", count: currentStats.retry }, { key: "retry", label: "Retry", count: currentStats.retry },
{ key: "archived", label: "Archived", count: currentStats.archived }, { key: "archived", label: "Archived", count: currentStats.archived },
{ key: "completed", label: "Completed", count: currentStats.completed },
]; ];
const [searchQuery, setSearchQuery] = useState<string>(""); const [searchQuery, setSearchQuery] = useState<string>("");
@ -229,6 +233,12 @@ function TasksTable(props: Props & ReduxProps) {
totalTaskCount={currentStats.archived} totalTaskCount={currentStats.archived}
/> />
</TabPanel> </TabPanel>
<TabPanel value="completed" selected={props.selected}>
<CompletedTasksTable
queue={props.queue}
totalTaskCount={currentStats.completed}
/>
</TabPanel>
</Paper> </Paper>
); );
} }

View File

@ -74,5 +74,11 @@ export default function queueStatsReducer(
// Returns true if two timestamps are from the same date. // Returns true if two timestamps are from the same date.
function isSameDate(ts1: string, ts2: string): boolean { function isSameDate(ts1: string, ts2: string): boolean {
return new Date(ts1).toDateString() === new Date(ts2).toDateString(); const date1 = new Date(ts1);
const date2 = new Date(ts2);
return (
date1.getUTCDate() === date2.getUTCDate() &&
date1.getUTCMonth() === date2.getUTCMonth() &&
date1.getUTCFullYear() === date2.getUTCFullYear()
);
} }

View File

@ -50,6 +50,9 @@ import {
BATCH_DELETE_PENDING_TASKS_SUCCESS, BATCH_DELETE_PENDING_TASKS_SUCCESS,
ARCHIVE_ALL_PENDING_TASKS_SUCCESS, ARCHIVE_ALL_PENDING_TASKS_SUCCESS,
DELETE_ALL_PENDING_TASKS_SUCCESS, DELETE_ALL_PENDING_TASKS_SUCCESS,
DELETE_COMPLETED_TASK_SUCCESS,
DELETE_ALL_COMPLETED_TASKS_SUCCESS,
BATCH_DELETE_COMPLETED_TASKS_SUCCESS,
} from "../actions/tasksActions"; } from "../actions/tasksActions";
import { Queue } from "../api"; import { Queue } from "../api";
@ -550,8 +553,7 @@ function queuesReducer(
queueInfo.currentStats.pending + queueInfo.currentStats.pending +
action.payload.archived_ids.length, action.payload.archived_ids.length,
retry: retry:
queueInfo.currentStats.retry - queueInfo.currentStats.retry - action.payload.archived_ids.length,
action.payload.archived_ids.length,
}, },
}; };
}); });
@ -647,6 +649,23 @@ function queuesReducer(
return { ...state, data: newData }; return { ...state, data: newData };
} }
case DELETE_COMPLETED_TASK_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
return queueInfo;
}
return {
...queueInfo,
currentStats: {
...queueInfo.currentStats,
size: queueInfo.currentStats.size - 1,
completed: queueInfo.currentStats.completed - 1,
},
};
});
return { ...state, data: newData };
}
case BATCH_RUN_ARCHIVED_TASKS_SUCCESS: { case BATCH_RUN_ARCHIVED_TASKS_SUCCESS: {
const newData = state.data.map((queueInfo) => { const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) { if (queueInfo.name !== action.queue) {
@ -688,6 +707,26 @@ function queuesReducer(
return { ...state, data: newData }; return { ...state, data: newData };
} }
case BATCH_DELETE_COMPLETED_TASKS_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
return queueInfo;
}
return {
...queueInfo,
currentStats: {
...queueInfo.currentStats,
size:
queueInfo.currentStats.size - action.payload.deleted_ids.length,
completed:
queueInfo.currentStats.completed -
action.payload.deleted_ids.length,
},
};
});
return { ...state, data: newData };
}
case RUN_ALL_ARCHIVED_TASKS_SUCCESS: { case RUN_ALL_ARCHIVED_TASKS_SUCCESS: {
const newData = state.data.map((queueInfo) => { const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) { if (queueInfo.name !== action.queue) {
@ -723,6 +762,23 @@ function queuesReducer(
return { ...state, data: newData }; return { ...state, data: newData };
} }
case DELETE_ALL_COMPLETED_TASKS_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
return queueInfo;
}
return {
...queueInfo,
currentStats: {
...queueInfo.currentStats,
size: queueInfo.currentStats.size - action.deleted,
completed: 0,
},
};
});
return { ...state, data: newData };
}
default: default:
return state; return state;
} }

View File

@ -36,6 +36,9 @@ import {
BATCH_DELETE_PENDING_TASKS_SUCCESS, BATCH_DELETE_PENDING_TASKS_SUCCESS,
ARCHIVE_ALL_PENDING_TASKS_SUCCESS, ARCHIVE_ALL_PENDING_TASKS_SUCCESS,
DELETE_ALL_PENDING_TASKS_SUCCESS, DELETE_ALL_PENDING_TASKS_SUCCESS,
DELETE_COMPLETED_TASK_SUCCESS,
DELETE_ALL_COMPLETED_TASKS_SUCCESS,
BATCH_DELETE_COMPLETED_TASKS_SUCCESS,
} from "../actions/tasksActions"; } from "../actions/tasksActions";
interface SnackbarState { interface SnackbarState {
@ -285,6 +288,25 @@ function snackbarReducer(
message: "All archived tasks deleted", message: "All archived tasks deleted",
}; };
case DELETE_COMPLETED_TASK_SUCCESS:
return {
isOpen: true,
message: `Completed task deleted`,
};
case DELETE_ALL_COMPLETED_TASKS_SUCCESS:
return {
isOpen: true,
message: "All completed tasks deleted",
};
case BATCH_DELETE_COMPLETED_TASKS_SUCCESS:
const n = action.payload.deleted_ids.length;
return {
isOpen: true,
message: `${n} completed ${n === 1 ? "task" : "tasks"} deleted`,
};
default: default:
return state; return state;
} }

View File

@ -15,6 +15,9 @@ import {
LIST_ARCHIVED_TASKS_BEGIN, LIST_ARCHIVED_TASKS_BEGIN,
LIST_ARCHIVED_TASKS_SUCCESS, LIST_ARCHIVED_TASKS_SUCCESS,
LIST_ARCHIVED_TASKS_ERROR, LIST_ARCHIVED_TASKS_ERROR,
LIST_COMPLETED_TASKS_BEGIN,
LIST_COMPLETED_TASKS_SUCCESS,
LIST_COMPLETED_TASKS_ERROR,
CANCEL_ACTIVE_TASK_BEGIN, CANCEL_ACTIVE_TASK_BEGIN,
CANCEL_ACTIVE_TASK_SUCCESS, CANCEL_ACTIVE_TASK_SUCCESS,
CANCEL_ACTIVE_TASK_ERROR, CANCEL_ACTIVE_TASK_ERROR,
@ -117,17 +120,19 @@ import {
GET_TASK_INFO_BEGIN, GET_TASK_INFO_BEGIN,
GET_TASK_INFO_ERROR, GET_TASK_INFO_ERROR,
GET_TASK_INFO_SUCCESS, GET_TASK_INFO_SUCCESS,
DELETE_COMPLETED_TASK_BEGIN,
DELETE_COMPLETED_TASK_ERROR,
DELETE_COMPLETED_TASK_SUCCESS,
DELETE_ALL_COMPLETED_TASKS_BEGIN,
DELETE_ALL_COMPLETED_TASKS_ERROR,
DELETE_ALL_COMPLETED_TASKS_SUCCESS,
BATCH_DELETE_COMPLETED_TASKS_BEGIN,
BATCH_DELETE_COMPLETED_TASKS_ERROR,
BATCH_DELETE_COMPLETED_TASKS_SUCCESS,
} from "../actions/tasksActions"; } from "../actions/tasksActions";
import { import { TaskInfo } from "../api";
ActiveTask,
ArchivedTask,
PendingTask,
RetryTask,
ScheduledTask,
TaskInfo,
} from "../api";
export interface ActiveTaskExtended extends ActiveTask { export interface ActiveTaskExtended extends TaskInfo {
// Indicates that a request has been sent for this // Indicates that a request has been sent for this
// task and awaiting for a response. // task and awaiting for a response.
requestPending: boolean; requestPending: boolean;
@ -137,25 +142,7 @@ export interface ActiveTaskExtended extends ActiveTask {
canceling: boolean; canceling: boolean;
} }
export interface PendingTaskExtended extends PendingTask { export interface TaskInfoExtended extends TaskInfo {
// Indicates that a request has been sent for this
// task and awaiting for a response.
requestPending: boolean;
}
export interface ScheduledTaskExtended extends ScheduledTask {
// Indicates that a request has been sent for this
// task and awaiting for a response.
requestPending: boolean;
}
export interface RetryTaskExtended extends RetryTask {
// Indicates that a request has been sent for this
// task and awaiting for a response.
requestPending: boolean;
}
export interface ArchivedTaskExtended extends ArchivedTask {
// Indicates that a request has been sent for this // Indicates that a request has been sent for this
// task and awaiting for a response. // task and awaiting for a response.
requestPending: boolean; requestPending: boolean;
@ -174,34 +161,41 @@ interface TasksState {
batchActionPending: boolean; batchActionPending: boolean;
allActionPending: boolean; allActionPending: boolean;
error: string; error: string;
data: PendingTaskExtended[]; data: TaskInfoExtended[];
}; };
scheduledTasks: { scheduledTasks: {
loading: boolean; loading: boolean;
batchActionPending: boolean; batchActionPending: boolean;
allActionPending: boolean; allActionPending: boolean;
error: string; error: string;
data: ScheduledTaskExtended[]; data: TaskInfoExtended[];
}; };
retryTasks: { retryTasks: {
loading: boolean; loading: boolean;
batchActionPending: boolean; batchActionPending: boolean;
allActionPending: boolean; allActionPending: boolean;
error: string; error: string;
data: RetryTaskExtended[]; data: TaskInfoExtended[];
}; };
archivedTasks: { archivedTasks: {
loading: boolean; loading: boolean;
batchActionPending: boolean; batchActionPending: boolean;
allActionPending: boolean; allActionPending: boolean;
error: string; error: string;
data: ArchivedTaskExtended[]; data: TaskInfoExtended[];
};
completedTasks: {
loading: boolean;
batchActionPending: boolean;
allActionPending: boolean;
error: string;
data: TaskInfoExtended[];
}; };
taskInfo: { taskInfo: {
loading: boolean; loading: boolean;
error: string; error: string;
data?: TaskInfo; data?: TaskInfo;
}, };
} }
const initialState: TasksState = { const initialState: TasksState = {
@ -240,10 +234,17 @@ const initialState: TasksState = {
error: "", error: "",
data: [], data: [],
}, },
completedTasks: {
loading: false,
batchActionPending: false,
allActionPending: false,
error: "",
data: [],
},
taskInfo: { taskInfo: {
loading: false, loading: false,
error: "", error: "",
} },
}; };
function tasksReducer( function tasksReducer(
@ -258,7 +259,7 @@ function tasksReducer(
...state.taskInfo, ...state.taskInfo,
loading: true, loading: true,
}, },
} };
case GET_TASK_INFO_ERROR: case GET_TASK_INFO_ERROR:
return { return {
@ -450,6 +451,157 @@ function tasksReducer(
}, },
}; };
case LIST_COMPLETED_TASKS_BEGIN:
return {
...state,
completedTasks: {
...state.completedTasks,
loading: true,
},
};
case LIST_COMPLETED_TASKS_SUCCESS:
return {
...state,
completedTasks: {
...state.completedTasks,
loading: false,
error: "",
data: action.payload.tasks.map((task) => ({
...task,
requestPending: false,
})),
},
};
case LIST_COMPLETED_TASKS_ERROR:
return {
...state,
completedTasks: {
...state.completedTasks,
loading: false,
error: action.error,
data: [],
},
};
case DELETE_COMPLETED_TASK_BEGIN:
return {
...state,
completedTasks: {
...state.completedTasks,
data: state.completedTasks.data.map((task) => {
if (task.id !== action.taskId) {
return task;
}
return { ...task, requestPending: true };
}),
},
};
case DELETE_COMPLETED_TASK_SUCCESS:
return {
...state,
completedTasks: {
...state.completedTasks,
data: state.completedTasks.data.filter(
(task) => task.id !== action.taskId
),
},
};
case DELETE_COMPLETED_TASK_ERROR:
return {
...state,
completedTasks: {
...state.completedTasks,
data: state.completedTasks.data.map((task) => {
if (task.id !== action.taskId) {
return task;
}
return { ...task, requestPending: false };
}),
},
};
case DELETE_ALL_COMPLETED_TASKS_BEGIN:
return {
...state,
completedTasks: {
...state.completedTasks,
allActionPending: true,
},
};
case DELETE_ALL_COMPLETED_TASKS_SUCCESS:
return {
...state,
completedTasks: {
...state.completedTasks,
allActionPending: false,
data: [],
},
};
case DELETE_ALL_COMPLETED_TASKS_ERROR:
return {
...state,
completedTasks: {
...state.completedTasks,
allActionPending: false,
},
};
case BATCH_DELETE_COMPLETED_TASKS_BEGIN:
return {
...state,
completedTasks: {
...state.completedTasks,
batchActionPending: true,
data: state.completedTasks.data.map((task) => {
if (!action.taskIds.includes(task.id)) {
return task;
}
return {
...task,
requestPending: true,
};
}),
},
};
case BATCH_DELETE_COMPLETED_TASKS_SUCCESS: {
const newData = state.completedTasks.data.filter(
(task) => !action.payload.deleted_ids.includes(task.id)
);
return {
...state,
completedTasks: {
...state.completedTasks,
batchActionPending: false,
data: newData,
},
};
}
case BATCH_DELETE_COMPLETED_TASKS_ERROR:
return {
...state,
completedTasks: {
...state.completedTasks,
batchActionPending: false,
data: state.completedTasks.data.map((task) => {
if (!action.taskIds.includes(task.id)) {
return task;
}
return {
...task,
requestPending: false,
};
}),
},
};
case CANCEL_ACTIVE_TASK_BEGIN: { case CANCEL_ACTIVE_TASK_BEGIN: {
const newData = state.activeTasks.data.map((task) => { const newData = state.activeTasks.data.map((task) => {
if (task.id !== action.taskId) { if (task.id !== action.taskId) {

View File

@ -25,17 +25,22 @@ interface Duration {
totalSeconds: number; totalSeconds: number;
} }
// start and end are in milliseconds. // Returns a duration from the number of seconds provided.
function durationBetween(start: number, end: number): Duration { export function durationFromSeconds(totalSeconds: number): Duration {
const durationInMillisec = start - end;
const totalSeconds = Math.floor(durationInMillisec / 1000);
const hour = Math.floor(totalSeconds / 3600); const hour = Math.floor(totalSeconds / 3600);
const minute = Math.floor((totalSeconds - 3600 * hour) / 60); const minute = Math.floor((totalSeconds - 3600 * hour) / 60);
const second = totalSeconds - 3600 * hour - 60 * minute; const second = totalSeconds - 3600 * hour - 60 * minute;
return { hour, minute, second, totalSeconds }; return { hour, minute, second, totalSeconds };
} }
function stringifyDuration(d: Duration): string { // start and end are in milliseconds.
function durationBetween(start: number, end: number): Duration {
const durationInMillisec = start - end;
const totalSeconds = Math.floor(durationInMillisec / 1000);
return durationFromSeconds(totalSeconds);
}
export function stringifyDuration(d: Duration): string {
if (d.hour > 24) { if (d.hour > 24) {
const n = Math.floor(d.hour / 24); const n = Math.floor(d.hour / 24);
return n + (n === 1 ? " day" : " days"); return n + (n === 1 ? " day" : " days");

View File

@ -18,6 +18,7 @@ import { TaskDetailsRouteParams } from "../paths";
import { usePolling } from "../hooks"; import { usePolling } from "../hooks";
import { listQueuesAsync } from "../actions/queuesActions"; import { listQueuesAsync } from "../actions/queuesActions";
import SyntaxHighlighter from "../components/SyntaxHighlighter"; import SyntaxHighlighter from "../components/SyntaxHighlighter";
import { durationFromSeconds, stringifyDuration, timeAgo } from "../utils";
function mapStateToProps(state: AppState) { function mapStateToProps(state: AppState) {
return { return {
@ -175,7 +176,7 @@ function TaskDetailsView(props: Props) {
{taskInfo?.error_message} ({taskInfo?.last_failed_at}) {taskInfo?.error_message} ({taskInfo?.last_failed_at})
</Typography> </Typography>
) : ( ) : (
<Typography>n/a</Typography> <Typography> - </Typography>
)} )}
</Typography> </Typography>
</div> </div>
@ -189,7 +190,7 @@ function TaskDetailsView(props: Props) {
{taskInfo?.next_process_at ? ( {taskInfo?.next_process_at ? (
<Typography>{taskInfo?.next_process_at}</Typography> <Typography>{taskInfo?.next_process_at}</Typography>
) : ( ) : (
<Typography>n/a</Typography> <Typography> - </Typography>
)} )}
</div> </div>
</div> </div>
@ -201,7 +202,7 @@ function TaskDetailsView(props: Props) {
{taskInfo?.timeout_seconds ? ( {taskInfo?.timeout_seconds ? (
<Typography>{taskInfo?.timeout_seconds} seconds</Typography> <Typography>{taskInfo?.timeout_seconds} seconds</Typography>
) : ( ) : (
<Typography>n/a</Typography> <Typography> - </Typography>
)} )}
</Typography> </Typography>
</div> </div>
@ -213,7 +214,7 @@ function TaskDetailsView(props: Props) {
{taskInfo?.deadline ? ( {taskInfo?.deadline ? (
<Typography>{taskInfo?.deadline}</Typography> <Typography>{taskInfo?.deadline}</Typography>
) : ( ) : (
<Typography>n/a</Typography> <Typography> - </Typography>
)} )}
</Typography> </Typography>
</div> </div>
@ -232,6 +233,59 @@ function TaskDetailsView(props: Props) {
)} )}
</div> </div>
</div> </div>
{
/* Completed Task Only */ taskInfo?.state === "completed" && (
<>
<div className={classes.infoRow}>
<Typography
variant="subtitle2"
className={classes.infoKeyCell}
>
Completed:{" "}
</Typography>
<div className={classes.infoValueCell}>
<Typography>
{timeAgo(taskInfo.completed_at)} (
{taskInfo.completed_at})
</Typography>
</div>
</div>
<div className={classes.infoRow}>
<Typography
variant="subtitle2"
className={classes.infoKeyCell}
>
Result:{" "}
</Typography>
<div className={classes.infoValueCell}>
<SyntaxHighlighter
language="json"
customStyle={{ margin: 0, maxWidth: 400 }}
>
{taskInfo.result}
</SyntaxHighlighter>
</div>
</div>
<div className={classes.infoRow}>
<Typography
variant="subtitle2"
className={classes.infoKeyCell}
>
TTL:{" "}
</Typography>
<Typography className={classes.infoValueCell}>
<Typography>
{taskInfo.ttl_seconds > 0
? `${stringifyDuration(
durationFromSeconds(taskInfo.ttl_seconds)
)} left`
: "expired"}
</Typography>
</Typography>
</div>
</>
)
}
</Paper> </Paper>
)} )}
<div className={classes.footer}> <div className={classes.footer}>

View File

@ -38,7 +38,14 @@ function useQuery(): URLSearchParams {
return new URLSearchParams(useLocation().search); return new URLSearchParams(useLocation().search);
} }
const validStatus = ["active", "pending", "scheduled", "retry", "archived"]; const validStatus = [
"active",
"pending",
"scheduled",
"retry",
"archived",
"completed",
];
const defaultStatus = "active"; const defaultStatus = "active";
function TasksView(props: ConnectedProps<typeof connector>) { function TasksView(props: ConnectedProps<typeof connector>) {

View File

@ -3399,9 +3399,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0" lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001181: caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001181:
version "1.0.30001192" version "1.0.30001271"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001192.tgz#b848ebc0ab230cf313d194a4775a30155d50ae40" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001271.tgz"
integrity sha512-63OrUnwJj5T1rUmoyqYTdRWBqFFxZFlyZnRRjDR8NSUQFB6A+j/uBORU/SyJ5WzDLg4SPiZH40hQCBNdZ/jmAw== integrity sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA==
capture-exit@^2.0.0: capture-exit@^2.0.0:
version "2.0.0" version "2.0.0"