mirror of
https://github.com/hibiken/asynqmon.git
synced 2025-01-18 18:55:54 +08:00
Add completed state
This commit is contained in:
parent
ddb1798ce8
commit
741a3c59fa
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
|
||||
|
7
Makefile
7
Makefile
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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
3
go.mod
@ -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
2
go.sum
@ -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=
|
||||
|
38
handler.go
38
handler.go
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
@ -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
1
ui/build/static/js/2.260e42b2.chunk.js.map
Normal file
1
ui/build/static/js/2.260e42b2.chunk.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
ui/build/static/js/main.525ff6d9.chunk.js
Normal file
2
ui/build/static/js/main.525ff6d9.chunk.js
Normal file
File diff suppressed because one or more lines are too long
1
ui/build/static/js/main.525ff6d9.chunk.js.map
Normal file
1
ui/build/static/js/main.525ff6d9.chunk.js.map
Normal file
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
@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
148
ui/src/api.ts
148
ui/src/api.ts
@ -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",
|
||||
|
@ -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;
|
||||
|
376
ui/src/components/CompletedTasksTable.tsx
Normal file
376
ui/src/components/CompletedTasksTable.tsx
Normal 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);
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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");
|
||||
|
@ -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}>
|
||||
|
@ -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>) {
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user