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
*.out
# main binary
# binaries
asynqmon
api
dist/
# Editor configs

View File

@ -1,10 +1,15 @@
.PHONY: assets build docker
.PHONY: api assets build docker
NODE_PATH ?= $(PWD)/ui/node_modules
assets:
@if [ ! -d "$(NODE_PATH)" ]; then cd ./ui && yarn install --modules-folder $(NODE_PATH); fi
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: assets
go build -o asynqmon ./cmd/asynqmon

View File

@ -150,7 +150,8 @@ func main() {
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.
log.Fatal(http.ListenAndServe(":8080", nil))

View File

@ -27,6 +27,7 @@ var (
flagRedisInsecureTLS bool
flagRedisClusterNodes string
flagMaxPayloadLength int
flagMaxResultLength int
)
func init() {
@ -39,6 +40,7 @@ func init() {
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.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.
@ -102,6 +104,7 @@ func main() {
h := asynqmon.New(asynqmon.Options{
RedisConnOpt: redisConnOpt,
PayloadFormatter: asynqmon.PayloadFormatterFunc(formatPayload),
ResultFormatter: asynqmon.ResultFormatterFunc(formatResult),
})
defer h.Close()
@ -125,6 +128,11 @@ func formatPayload(taskType string, payload []byte) string {
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).
func truncate(s string, limit int) string {
i := 0

View File

@ -14,7 +14,7 @@ import (
// - 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 {
// FormatPayload takes the task's typename and payload and returns a string representation of the payload.
FormatPayload(taskType string, payload []byte) string
@ -22,11 +22,22 @@ type PayloadFormatter interface {
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 {
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.
// 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.
@ -37,6 +48,16 @@ var DefaultPayloadFormatter = PayloadFormatterFunc(func(_ string, payload []byte
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.
func isPrintable(data []byte) bool {
if !utf8.Valid(data) {
@ -67,6 +88,7 @@ type queueStateSnapshot struct {
Scheduled int `json:"scheduled"`
Retry int `json:"retry"`
Archived int `json:"archived"`
Completed int `json:"completed"`
// Total number of tasks processed during the given date.
// The number includes both succeeded and failed tasks.
@ -91,6 +113,7 @@ func toQueueStateSnapshot(s *asynq.QueueInfo) *queueStateSnapshot {
Scheduled: s.Scheduled,
Retry: s.Retry,
Archived: s.Archived,
Completed: s.Completed,
Processed: s.Processed,
Succeeded: s.Processed - 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.
// If not applicable, empty string.
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.
@ -163,7 +202,7 @@ func formatTimeInRFC3339(t time.Time) string {
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{
ID: info.ID,
Queue: info.Queue,
@ -177,6 +216,9 @@ func toTaskInfo(info *asynq.TaskInfo, pf PayloadFormatter) *taskInfo {
Timeout: int(info.Timeout.Seconds()),
Deadline: formatTimeInRFC3339(info.Deadline),
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
}
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 {
ID string `json:"id"`
Spec string `json:"spec"`

3
go.mod
View File

@ -5,6 +5,7 @@ go 1.16
require (
github.com/go-redis/redis/v8 v8.11.3
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
)

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/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.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/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
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.
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.
@ -78,8 +83,9 @@ func (h *HTTPHandler) Close() error {
}
// RootPath returns the root URL path used for asynqmon application.
// Returned path string does not have the trailing slash.
func (h *HTTPHandler) RootPath() string {
return h.rootPath + "/"
return h.rootPath
}
//go:embed ui/build/*
@ -88,9 +94,14 @@ var staticContents embed.FS
func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspector) *mux.Router {
router := mux.NewRouter().PathPrefix(opts.RootPath).Subrouter()
var pf PayloadFormatter = DefaultPayloadFormatter
var payloadFmt PayloadFormatter = DefaultPayloadFormatter
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()
@ -105,12 +116,12 @@ func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspecto
api.HandleFunc("/queue_stats", newListQueueStatsHandlerFunc(inspector)).Methods("GET")
// 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:cancel_all", newCancelAllActiveTasksHandlerFunc(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:delete_all", newDeleteAllPendingTasksHandlerFunc(inspector)).Methods("DELETE")
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: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:delete_all", newDeleteAllScheduledTasksHandlerFunc(inspector)).Methods("DELETE")
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: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:delete_all", newDeleteAllRetryTasksHandlerFunc(inspector)).Methods("DELETE")
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: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:delete_all", newDeleteAllArchivedTasksHandlerFunc(inspector)).Methods("DELETE")
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: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.
api.HandleFunc("/servers", newListServersHandlerFunc(inspector, pf)).Methods("GET")
api.HandleFunc("/servers", newListServersHandlerFunc(inspector, payloadFmt)).Methods("GET")
// 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")
// 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 {
return func(w http.ResponseWriter, r *http.Request) {
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 {
return func(w http.ResponseWriter, r *http.Request) {
qname := mux.Vars(r)["qname"]
@ -627,7 +673,7 @@ func getPageOptions(r *http.Request) (pageSize, pageNum int) {
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) {
vars := mux.Vars(r)
qname, taskid := vars["qname"], vars["task_id"]
@ -650,7 +696,7 @@ func newGetTaskHandlerFunc(inspector *asynq.Inspector, pf PayloadFormatter) http
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)
return
}

View File

@ -1,17 +1,17 @@
{
"files": {
"main.js": "/[[.RootPath]]/static/js/main.dec9d0fd.chunk.js",
"main.js.map": "/[[.RootPath]]/static/js/main.dec9d0fd.chunk.js.map",
"main.js": "/[[.RootPath]]/static/js/main.525ff6d9.chunk.js",
"main.js.map": "/[[.RootPath]]/static/js/main.525ff6d9.chunk.js.map",
"runtime-main.js": "/[[.RootPath]]/static/js/runtime-main.9fea6c1a.js",
"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.3f9a2354.chunk.js.map": "/[[.RootPath]]/static/js/2.3f9a2354.chunk.js.map",
"static/js/2.260e42b2.chunk.js": "/[[.RootPath]]/static/js/2.260e42b2.chunk.js",
"static/js/2.260e42b2.chunk.js.map": "/[[.RootPath]]/static/js/2.260e42b2.chunk.js.map",
"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": [
"static/js/runtime-main.9fea6c1a.js",
"static/js/2.3f9a2354.chunk.js",
"static/js/main.dec9d0fd.chunk.js"
"static/js/2.260e42b2.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,
batchDeleteRetryTasks,
batchDeleteScheduledTasks,
batchDeleteCompletedTasks,
BatchDeleteTasksResponse,
batchArchiveRetryTasks,
batchArchiveScheduledTasks,
@ -17,23 +18,22 @@ import {
deleteAllArchivedTasks,
deleteAllRetryTasks,
deleteAllScheduledTasks,
deleteAllCompletedTasks,
deleteArchivedTask,
deleteRetryTask,
deleteScheduledTask,
deleteCompletedTask,
archiveAllRetryTasks,
archiveAllScheduledTasks,
archiveRetryTask,
archiveScheduledTask,
listActiveTasks,
ListActiveTasksResponse,
listArchivedTasks,
ListArchivedTasksResponse,
listPendingTasks,
ListPendingTasksResponse,
ListTasksResponse,
listRetryTasks,
ListRetryTasksResponse,
listScheduledTasks,
ListScheduledTasksResponse,
listCompletedTasks,
PaginationOptions,
runAllArchivedTasks,
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_SUCCESS = "LIST_ARCHIVED_TASKS_SUCCESS";
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_SUCCESS = "CANCEL_ACTIVE_TASK_SUCCESS";
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";
export const 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 {
type: typeof GET_TASK_INFO_BEGIN;
@ -236,7 +254,7 @@ interface ListActiveTasksBeginAction {
interface ListActiveTasksSuccessAction {
type: typeof LIST_ACTIVE_TASKS_SUCCESS;
queue: string;
payload: ListActiveTasksResponse;
payload: ListTasksResponse;
}
interface ListActiveTasksErrorAction {
@ -253,7 +271,7 @@ interface ListPendingTasksBeginAction {
interface ListPendingTasksSuccessAction {
type: typeof LIST_PENDING_TASKS_SUCCESS;
queue: string;
payload: ListPendingTasksResponse;
payload: ListTasksResponse;
}
interface ListPendingTasksErrorAction {
@ -270,7 +288,7 @@ interface ListScheduledTasksBeginAction {
interface ListScheduledTasksSuccessAction {
type: typeof LIST_SCHEDULED_TASKS_SUCCESS;
queue: string;
payload: ListScheduledTasksResponse;
payload: ListTasksResponse;
}
interface ListScheduledTasksErrorAction {
@ -287,7 +305,7 @@ interface ListRetryTasksBeginAction {
interface ListRetryTasksSuccessAction {
type: typeof LIST_RETRY_TASKS_SUCCESS;
queue: string;
payload: ListRetryTasksResponse;
payload: ListTasksResponse;
}
interface ListRetryTasksErrorAction {
@ -304,7 +322,7 @@ interface ListArchivedTasksBeginAction {
interface ListArchivedTasksSuccessAction {
type: typeof LIST_ARCHIVED_TASKS_SUCCESS;
queue: string;
payload: ListArchivedTasksResponse;
payload: ListTasksResponse;
}
interface ListArchivedTasksErrorAction {
@ -313,6 +331,23 @@ interface ListArchivedTasksErrorAction {
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 {
type: typeof CANCEL_ACTIVE_TASK_BEGIN;
queue: string;
@ -911,6 +946,61 @@ interface DeleteAllArchivedTasksErrorAction {
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.
export type TasksActionTypes =
| GetTaskInfoBeginAction
@ -931,6 +1021,9 @@ export type TasksActionTypes =
| ListArchivedTasksBeginAction
| ListArchivedTasksSuccessAction
| ListArchivedTasksErrorAction
| ListCompletedTasksBeginAction
| ListCompletedTasksSuccessAction
| ListCompletedTasksErrorAction
| CancelActiveTaskBeginAction
| CancelActiveTaskSuccessAction
| CancelActiveTaskErrorAction
@ -1029,7 +1122,16 @@ export type TasksActionTypes =
| RunAllArchivedTasksErrorAction
| DeleteAllArchivedTasksBeginAction
| DeleteAllArchivedTasksSuccessAction
| DeleteAllArchivedTasksErrorAction;
| DeleteAllArchivedTasksErrorAction
| DeleteCompletedTaskBeginAction
| DeleteCompletedTaskSuccessAction
| DeleteCompletedTaskErrorAction
| BatchDeleteCompletedTasksBeginAction
| BatchDeleteCompletedTasksSuccessAction
| BatchDeleteCompletedTasksErrorAction
| DeleteAllCompletedTasksBeginAction
| DeleteAllCompletedTasksSuccessAction
| DeleteAllCompletedTasksErrorAction;
export function getTaskInfoAsync(qname: string, id: string) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
@ -1039,15 +1141,15 @@ export function getTaskInfoAsync(qname: string, id: string) {
dispatch({
type: GET_TASK_INFO_SUCCESS,
payload: response,
})
});
} catch (error) {
console.error("getTaskInfoAsync: ", toErrorStringWithHttpStatus(error));
dispatch({
type: GET_TASK_INFO_ERROR,
error: toErrorString(error),
})
});
}
}
};
}
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) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: CANCEL_ACTIVE_TASK_BEGIN, queue, taskId });
@ -1395,10 +1524,7 @@ export function deletePendingTaskAsync(queue: string, taskId: string) {
};
}
export function batchDeletePendingTasksAsync(
queue: string,
taskIds: string[]
) {
export function batchDeletePendingTasksAsync(queue: string, taskIds: string[]) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: BATCH_DELETE_PENDING_TASKS_BEGIN, queue, taskIds });
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.
// In developement, we assume that the API server is listening on port 8080.
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 {
queues: Queue[];
}
export interface ListActiveTasksResponse {
tasks: ActiveTask[];
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[];
export interface ListTasksResponse {
tasks: TaskInfo[];
stats: Queue;
}
@ -239,6 +221,7 @@ export interface Queue {
scheduled: number;
retry: number;
archived: number;
completed: number;
processed: number;
failed: number;
timestamp: string;
@ -251,18 +234,13 @@ export interface DailyStat {
failed: number;
}
// BaseTask corresponds to asynq.Task type.
interface BaseTask {
type: string;
payload: string;
}
export interface TaskInfo {
id: string;
queue: string;
type: string;
payload: string;
state: string;
start_time: string; // Only applies to task.state == 'active'
max_retry: number;
retried: number;
last_failed_at: string;
@ -270,51 +248,9 @@ export interface TaskInfo {
next_process_at: string;
timeout_seconds: number;
deadline: string;
}
export interface ActiveTask extends BaseTask {
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;
completed_at: string;
result: string;
ttl_seconds: number;
}
export interface ServerInfo {
@ -396,7 +332,10 @@ export async function listQueueStats(): Promise<ListQueueStatsResponse> {
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 resp = await axios({
method: "get",
@ -408,7 +347,7 @@ export async function getTaskInfo(qname: string, id: string): Promise<TaskInfo>
export async function listActiveTasks(
qname: string,
pageOpts?: PaginationOptions
): Promise<ListActiveTasksResponse> {
): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/active_tasks`;
if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`;
@ -454,7 +393,7 @@ export async function batchCancelActiveTasks(
export async function listPendingTasks(
qname: string,
pageOpts?: PaginationOptions
): Promise<ListPendingTasksResponse> {
): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/pending_tasks`;
if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`;
@ -469,7 +408,7 @@ export async function listPendingTasks(
export async function listScheduledTasks(
qname: string,
pageOpts?: PaginationOptions
): Promise<ListScheduledTasksResponse> {
): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/scheduled_tasks`;
if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`;
@ -484,7 +423,7 @@ export async function listScheduledTasks(
export async function listRetryTasks(
qname: string,
pageOpts?: PaginationOptions
): Promise<ListRetryTasksResponse> {
): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/retry_tasks`;
if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`;
@ -499,7 +438,7 @@ export async function listRetryTasks(
export async function listArchivedTasks(
qname: string,
pageOpts?: PaginationOptions
): Promise<ListArchivedTasksResponse> {
): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/archived_tasks`;
if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`;
@ -511,6 +450,21 @@ export async function listArchivedTasks(
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(
qname: 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> {
const resp = await axios({
method: "get",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -74,5 +74,11 @@ export default function queueStatsReducer(
// Returns true if two timestamps are from the same date.
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,
ARCHIVE_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";
import { Queue } from "../api";
@ -550,8 +553,7 @@ function queuesReducer(
queueInfo.currentStats.pending +
action.payload.archived_ids.length,
retry:
queueInfo.currentStats.retry -
action.payload.archived_ids.length,
queueInfo.currentStats.retry - action.payload.archived_ids.length,
},
};
});
@ -647,6 +649,23 @@ function queuesReducer(
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: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
@ -688,6 +707,26 @@ function queuesReducer(
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: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
@ -723,6 +762,23 @@ function queuesReducer(
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:
return state;
}

View File

@ -36,6 +36,9 @@ import {
BATCH_DELETE_PENDING_TASKS_SUCCESS,
ARCHIVE_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";
interface SnackbarState {
@ -285,6 +288,25 @@ function snackbarReducer(
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:
return state;
}

View File

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

View File

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

View File

@ -18,6 +18,7 @@ import { TaskDetailsRouteParams } from "../paths";
import { usePolling } from "../hooks";
import { listQueuesAsync } from "../actions/queuesActions";
import SyntaxHighlighter from "../components/SyntaxHighlighter";
import { durationFromSeconds, stringifyDuration, timeAgo } from "../utils";
function mapStateToProps(state: AppState) {
return {
@ -175,7 +176,7 @@ function TaskDetailsView(props: Props) {
{taskInfo?.error_message} ({taskInfo?.last_failed_at})
</Typography>
) : (
<Typography>n/a</Typography>
<Typography> - </Typography>
)}
</Typography>
</div>
@ -189,7 +190,7 @@ function TaskDetailsView(props: Props) {
{taskInfo?.next_process_at ? (
<Typography>{taskInfo?.next_process_at}</Typography>
) : (
<Typography>n/a</Typography>
<Typography> - </Typography>
)}
</div>
</div>
@ -201,7 +202,7 @@ function TaskDetailsView(props: Props) {
{taskInfo?.timeout_seconds ? (
<Typography>{taskInfo?.timeout_seconds} seconds</Typography>
) : (
<Typography>n/a</Typography>
<Typography> - </Typography>
)}
</Typography>
</div>
@ -213,7 +214,7 @@ function TaskDetailsView(props: Props) {
{taskInfo?.deadline ? (
<Typography>{taskInfo?.deadline}</Typography>
) : (
<Typography>n/a</Typography>
<Typography> - </Typography>
)}
</Typography>
</div>
@ -232,6 +233,59 @@ function TaskDetailsView(props: Props) {
)}
</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>
)}
<div className={classes.footer}>

View File

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

View File

@ -1546,7 +1546,7 @@
native-url "^0.2.6"
schema-utils "^2.6.5"
source-map "^0.7.3"
"@reduxjs/toolkit@1.6.2":
version "1.6.2"
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.6.2.tgz#2f2b5365df77dd6697da28fdf44f33501ed9ba37"
@ -3399,9 +3399,9 @@ caniuse-api@^3.0.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:
version "1.0.30001192"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001192.tgz#b848ebc0ab230cf313d194a4775a30155d50ae40"
integrity sha512-63OrUnwJj5T1rUmoyqYTdRWBqFFxZFlyZnRRjDR8NSUQFB6A+j/uBORU/SyJ5WzDLg4SPiZH40hQCBNdZ/jmAw==
version "1.0.30001271"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001271.tgz"
integrity sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA==
capture-exit@^2.0.0:
version "2.0.0"