Compare commits

...

78 Commits

Author SHA1 Message Date
Ken Hibino
d1b889456d Fix docker build 2023-07-03 20:10:05 -07:00
Phước Trung
e44ba437a4 upgrade Redis client to v9 2023-07-03 14:54:34 -07:00
csgeek
5c48e4e31d Update README.md
Updating README to include a code example for integration into echo web server.  

Related issue: https://github.com/hibiken/asynqmon/issues/187 and credit to: https://github.com/tempor1s for solution.
2022-12-18 11:44:04 -08:00
Arne Zeising
2cb4c8c1bc Update static.go 2022-12-18 11:42:45 -08:00
Arne Zeising
3f4e7615fb Add MIME type to served files 2022-12-18 11:42:45 -08:00
lengcharles
bda90ac732 docs: fix one typo 2022-09-04 09:33:43 -07:00
Ken Hibino
1597dac66e Update readme 2022-05-06 05:50:51 -07:00
Ken Hibino
b3b8c2d13d Add and fix comments 2022-05-06 05:38:26 -07:00
Ken Hibino
b7c2ebeff3 Refactor flag parsing code 2022-05-06 05:38:26 -07:00
Ken Hibino
0527b6c483 Add sentinel connection support via redis-url 2022-05-06 05:38:26 -07:00
Ken Hibino
6dbc580738 Add tests for asynqmon command binary 2022-05-06 05:38:26 -07:00
Ken Hibino
2f9d2021c3 Refactor flag parsing 2022-05-06 05:38:26 -07:00
Ken Hibino
9796da746b Update version compatibility table in README 2022-04-11 17:50:35 -07:00
Ken Hibino
9ef529e8c5 v0.7.0 2022-04-11 17:49:50 -07:00
Ken Hibino
8a73386bd7 Update build assets 2022-04-11 17:20:26 -07:00
Ken Hibino
6fbf82f3e2 Update asynq dep version 2022-04-11 17:20:26 -07:00
Ken Hibino
7e0ae2b4a6 (ui): Use TasksTable component for aggregating tasks 2022-04-11 17:20:26 -07:00
Ken Hibino
c22c0206d7 (ui): Convert all tasks tables to use TasksTable component 2022-04-11 17:20:26 -07:00
Ken Hibino
726d58fcda (ui): Add renderRow prop to TasksTable component 2022-04-11 17:20:26 -07:00
Ken Hibino
6387d9cc67 (ui): Make task action props optional 2022-04-11 17:20:26 -07:00
Ken Hibino
97d969171f Create generic TasksTable component 2022-04-11 17:20:26 -07:00
Ken Hibino
cd6947ef20 (ui): Rename TasksTable to TasksTableContainer 2022-04-11 17:20:26 -07:00
Ken Hibino
bb26dda300 (ui): Reduce bundle size by using light syntax-highlighter 2022-04-11 17:20:26 -07:00
Ken Hibino
d0a8b6b691 (ui): Fix inconsistent data shown in AggregatingTasks view 2022-04-11 17:20:26 -07:00
Ken Hibino
9de7f054bc (ui): Update queue state when fetching aggregating tasks 2022-04-11 17:20:26 -07:00
Ken Hibino
0a6150e935 (ui): Update snackbar state upon aggregating task actions 2022-04-11 17:20:26 -07:00
Ken Hibino
5d9e4aec9c (ui): Add empty group UI 2022-04-11 17:20:26 -07:00
Ken Hibino
c8d7da05eb (ui): Update Queue state when new list of groups is fetched 2022-04-11 17:20:26 -07:00
Ken Hibino
33e76f263d (ui): Add action buttons to AggregatingTasksTable 2022-04-11 17:20:26 -07:00
Ken Hibino
ad20a8a7e7 Add writeResponseJSON helper 2022-04-11 17:20:26 -07:00
Ken Hibino
b9254e8c65 Add redux actions/reducer for aggregating task actions 2022-04-11 17:20:26 -07:00
Ken Hibino
c139200b10 (ui): Add api functions for aggregating task actions 2022-04-11 17:20:26 -07:00
Ken Hibino
28b1d463d0 Add REST endpoints for actions on aggregating_tasks 2022-04-11 17:20:26 -07:00
Ken Hibino
ff63a289a2 (ui): minor cleanup 2022-04-11 17:20:26 -07:00
Ken Hibino
ad687c4dc7 (ui): Add error and not found alert box in AggregatingTasksTable 2022-04-11 17:20:26 -07:00
Ken Hibino
c13cba0d5d (ui): Rename TaskGroupsTable to AggregatingTasksTable 2022-04-11 17:20:26 -07:00
Ken Hibino
db8b77591e (ui): Display tasks in TaskGroupsTable 2022-04-11 17:20:26 -07:00
Ken Hibino
a479098bd6 (ui): Add redux action/reducer for aggregating tasks 2022-04-11 17:20:26 -07:00
Ken Hibino
f6d84b1dc2 Add REST endpoint for listing aggregating tasks 2022-04-11 17:20:26 -07:00
Ken Hibino
e4b7765277 WIP: Add table to TaskGroupsTable 2022-04-11 17:20:26 -07:00
Ken Hibino
55de4e84eb (ui): Make GroupSelect controlled input 2022-04-11 17:20:26 -07:00
Ken Hibino
f31f248937 Use virtualized list 2022-04-11 17:20:26 -07:00
Ken Hibino
99f147df66 (ui): Add react-window 2022-04-11 17:20:26 -07:00
Ken Hibino
3cd2cbe6f2 Add GroupSelect component 2022-04-11 17:20:26 -07:00
Ken Hibino
b7667d8c7b Fix QueueInfo conversion helper 2022-04-11 17:20:26 -07:00
Ken Hibino
81eed7e33d Add redux actions/reducers for groups 2022-04-11 17:20:26 -07:00
Ken Hibino
33b24ca940 Add list groups REST endpoint 2022-04-11 17:20:26 -07:00
Ken Hibino
a2b6925041 Update QueueSizeChart to show aggregating task count 2022-04-11 17:20:26 -07:00
Ken Hibino
568e2e301c Fix TasksTable styles 2022-04-11 17:20:26 -07:00
Ken Hibino
a0e80ca4da Add group count and aggregating task count in TaskView 2022-04-11 17:20:26 -07:00
Ken Hibino
6ec87cd434 v0.6.1 2022-03-17 06:16:07 -07:00
Ken Hibino
56976997d2 (fix): Show metrics link when --prometheus-addr is provided 2022-03-17 06:13:10 -07:00
Ken Hibino
d31a42d85d Update readme 2022-03-12 15:34:44 -08:00
Ken Hibino
fc7b4a10bf v0.6.0 2022-03-02 06:36:30 -08:00
Ken Hibino
3e9365882d Update ui build files 2022-03-02 06:34:45 -08:00
Ken Hibino
14effdde06 (ui): Fix timeAgo helper to return dash when zero time is passed 2022-03-02 06:34:45 -08:00
Ken Hibino
1601a0861a Use logo image 2022-03-02 06:34:45 -08:00
Ken Hibino
fe6898e75e Round queue latency displayed in UI to the nearest multiple of 10ms 2022-03-02 06:34:45 -08:00
Ken Hibino
af9c47d038 Fix pagination in Active table 2022-03-02 06:34:45 -08:00
Ken Hibino
834a759680 Update ui build 2022-03-02 06:34:45 -08:00
Ken Hibino
1a27aaacbe Add concept of Flag values under window object to ensure values are
parsed before use
2022-03-02 06:34:45 -08:00
Ken Hibino
2991ea5a60 Update changelog 2022-03-02 06:34:45 -08:00
Ken Hibino
15e1eaaa56 Update ui build files 2022-03-02 06:34:45 -08:00
Ken Hibino
49eece97f7 (ui): Hide action buttons in read-only mode 2022-03-02 06:34:45 -08:00
Ken Hibino
3805ae6e06 (cmd): Add --read-only mode flag 2022-03-02 06:34:45 -08:00
Ken Hibino
c04e63d3f7 Add ReadOnly option for HTTPHandler 2022-03-02 06:34:45 -08:00
Ken Hibino
ade2baceaf Update changelog 2022-03-02 06:34:45 -08:00
Ken Hibino
8a508c58eb Display queue latency 2022-03-02 06:34:45 -08:00
Ken Hibino
0d58ef86f4 Update asynq dependency to v0.22.0 2022-03-02 06:34:45 -08:00
Ken Hibino
aceac82d78 Show orphaned status in active task table 2022-03-02 06:34:45 -08:00
Peizhi Zheng
1655bf3d88 address comments 2022-03-02 06:34:45 -08:00
Peizhi Zheng
91683248d0 fix jsx config error 2022-03-02 06:34:45 -08:00
Peizhi Zheng
3d31c94258 add copy id for the rest of tables 2022-03-02 06:34:45 -08:00
Peizhi Zheng
747d10df97 add copy id feature for task tables 2022-03-02 06:34:45 -08:00
Peizhi Zheng
609b319a9e
Add copy taskID to clipboard button 2022-01-14 22:46:49 -08:00
Ken Hibino
3ae79d85c3 Fix typescript error 2021-12-20 07:11:15 -08:00
Ken Hibino
e98f285767
Add metrics view screenshot in readme 2021-12-19 16:38:51 -08:00
Ken Hibino
e74815a7c1 Update changelog 2021-12-19 16:32:10 -08:00
62 changed files with 4222 additions and 2625 deletions

5
.gitignore vendored
View File

@ -24,8 +24,9 @@ package-json.lock
*.out
# binaries
asynqmon
api
/cmd/asynqmon/asynqmon
/asynqmon
/api
dist/
# Editor configs

View File

@ -7,6 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.7.0] - 2022-04-11
Version 0.7 added support for [Task Aggregation](https://github.com/hibiken/asynq/wiki/Task-aggregation) feature
### Added
- (ui): Added tasks view to show aggregated tasks
## [0.6.1] - 2022-03-17
### Fixed
- (ui): Show metrics link in sidebar when --prometheus-addr flag is provided
## [0.6.0] - 2022-03-02
### Added
- (cmd): Added `--read-only` flag to specify read-only mode
- (pkg): Added `Options.ReadOnly` to restrict user to view-only mode
- (ui): Hide action buttons in read-only mode
- (ui): Display queue latency in dashboard page and queue detail page.
- (ui): Added copy-to-clipboard button for task ID in tasks list-view page.
- (ui): Use logo image in the appbar (thank you @koddr!)
### Fixed
- (ui): Pagination in ActiveTasks table is fixed
## [0.5.0] - 2021-12-19
Version 0.5 added support for [Prometheus](https://prometheus.io/) integration.
- (cmd): Added `--enable-metrics-exporter` option to export queue metrics.
- (cmd): Added `--prometheus-addr` to enable metrics view in Web UI.
- (pkg): Added `Options.PrometheusAddress` to enable metrics view in Web UI.
## [0.4.0] - 2021-11-06
- Added "completed" state

View File

@ -3,13 +3,15 @@
# Building a frontend.
#
FROM alpine:3.13 AS frontend
FROM alpine:3.17 AS frontend
# Move to a working directory (/static).
WORKDIR /static
# https://stackoverflow.com/questions/69692842/error-message-error0308010cdigital-envelope-routinesunsupported
ENV NODE_OPTIONS=--openssl-legacy-provider
# Install npm (with latest nodejs) and yarn (globally, in silent mode).
RUN apk add --no-cache npm && \
RUN apk add --update nodejs npm && \
npm i -g -s --unsafe-perm yarn
# Copy only ./ui folder to the working directory.
@ -23,7 +25,7 @@ RUN yarn install && yarn build
# Building a backend.
#
FROM golang:1.16-alpine AS backend
FROM golang:1.18-alpine AS backend
# Move to a working directory (/build).
WORKDIR /build

View File

@ -1,6 +1,6 @@
<img src="https://user-images.githubusercontent.com/11155743/114745460-57760500-9d57-11eb-9a2c-43fa88171807.png" alt="Asynqmon logo" width="360px" />
# A modern web based tool for monitoring & administering [Asynq](https://github.com/hibiken/asynq) queues and tasks
# Web UI for monitoring & administering [Asynq](https://github.com/hibiken/asynq) task queue
## Overview
@ -15,7 +15,9 @@ Please make sure the version compatibility with the Asynq package you are using.
| Asynq version | WebUI (asynqmon) version |
| -------------- | ------------------------ |
| 0.20.x | 0.5.x |
| 0.23.x | 0.7.x |
| 0.22.x | 0.6.x |
| 0.20.x, 0.21.x | 0.5.x |
| 0.19.x | 0.4.x |
| 0.18.x | 0.2.x, 0.3.x |
| 0.16.x, 0.17.x | 0.1.x |
@ -96,18 +98,47 @@ Here's the available flags:
_Note_: Use `--redis-url` to specify address, db-number, and password with one flag value; Alternatively, use `--redis-addr`, `--redis-db`, and `--redis-password` to specify each value.
| Flag | Env | Description | Default |
| --------------------------------- | ------------------------- | ------------------------------------------------------------------- | ---------------- |
| `--port`(int) | `PORT` | port number to use for web ui server | 8080 |
| `---redis-url`(string) | `REDIS_URL` | URL to redis server | "" |
| `--redis-addr`(string) | `REDIS_ADDR` | address of redis server to connect to | "127.0.0.1:6379" |
| `--redis-db`(int) | `REDIS_DB` | redis database number | 0 |
| `--redis-password`(string) | `REDIS_PASSWORD` | password to use when connecting to redis server | "" |
| `--redis-cluster-nodes`(string) | `REDIS_CLUSTER_NODES` | comma separated list of host:port addresses of cluster nodes | "" |
| `--redis-tls`(string) | `REDIS_TLS` | server name for TLS validation used when connecting to redis server | "" |
| `--redis-insecure-tls`(bool) | `REDIS_INSECURE_TLS` | disable TLS certificate host checks | false |
| `--enable-metrics-exporter`(bool) | `ENABLE_METRICS_EXPORTER` | enable prometheus metrics exporter to expose queue metrics | false |
| `--prometheus-addr`(string) | `PROMETHEUS_ADDR` | address of prometheus server to query time series | "" |
| Flag | Env | Description | Default |
| --------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| `--port`(int) | `PORT` | port number to use for web ui server | 8080 |
| `---redis-url`(string) | `REDIS_URL` | URL to redis or sentinel server. See [godoc](https://pkg.go.dev/github.com/hibiken/asynq#ParseRedisURI) for supported format | "" |
| `--redis-addr`(string) | `REDIS_ADDR` | address of redis server to connect to | "127.0.0.1:6379" |
| `--redis-db`(int) | `REDIS_DB` | redis database number | 0 |
| `--redis-password`(string) | `REDIS_PASSWORD` | password to use when connecting to redis server | "" |
| `--redis-cluster-nodes`(string) | `REDIS_CLUSTER_NODES` | comma separated list of host:port addresses of cluster nodes | "" |
| `--redis-tls`(string) | `REDIS_TLS` | server name for TLS validation used when connecting to redis server | "" |
| `--redis-insecure-tls`(bool) | `REDIS_INSECURE_TLS` | disable TLS certificate host checks | false |
| `--enable-metrics-exporter`(bool) | `ENABLE_METRICS_EXPORTER` | enable prometheus metrics exporter to expose queue metrics | false |
| `--prometheus-addr`(string) | `PROMETHEUS_ADDR` | address of prometheus server to query time series | "" |
| `--read-only`(bool) | `READ_ONLY` | use web UI in read-only mode | false |
### Connecting to Redis
To connect to a **single redis server**, use either `--redis-url` or (`--redis-addr`, `--redis-db`, and `--redis-password`).
Example:
```sh
$ ./asynqmon --redis-url=redis://:mypassword@localhost:6380/2
$ ./asynqmon --redis-addr=localhost:6380 --redis-db=2 --redis-password=mypassword
```
To connect to **redis-sentinels**, use `--redis-url`.
Example:
```sh
$ ./asynqmon --redis-url=redis-sentinel://:mypassword@localhost:5000,localhost:5001,localhost:5002?master=mymaster
```
To connect to a **redis-cluster**, use `--redis-cluster-nodes`.
Example:
```sh
$ ./asynqmon --redis-cluster-nodes=localhost:7000,localhost:7001,localhost:7002,localhost:7003,localhost:7004,localhost:7006
```
### Integration with Prometheus
@ -119,6 +150,8 @@ The metrics data is now available under `/metrics` for Prometheus server to scra
Once the metrics data is collected by a Prometheus server, you can pass the address of the Prometheus server to asynqmon to query the time-series data.
The address can be specified via `--prometheus-addr`. This enables the metrics view on the Web UI.
<img width="1532" alt="Screen Shot 2021-12-19 at 4 37 19 PM" src="https://user-images.githubusercontent.com/10953044/146696852-25916465-07f0-4ed5-af31-18be02390bcb.png">
### Examples
```bash
@ -220,6 +253,35 @@ func main() {
}
```
Example with [labstack/echo](https://github.com/labstack/echo)):
```go
package main
import (
"github.com/labstack/echo/v4"
"github.com/hibiken/asynq"
"github.com/hibiken/asynqmon"
)
func main() {
e := echo.New()
mon := asynqmon.New(asynqmon.Options{
RootPath: "/monitoring/tasks",
RedisConnOpt: asynq.RedisClientOpt{
Addr: ":6379",
Password: "",
DB: 0,
},
})
e.Any("/monitoring/tasks/*", echo.WrapHandler(mon))
e.Start(":8080")
}
```
## License
Copyright (c) 2019-present [Ken Hibino](https://github.com/hibiken) and [Contributors](https://github.com/hibiken/asynqmon/graphs/contributors). `Asynqmon` is free and open-source software licensed under the [MIT License](https://github.com/hibiken/asynq/blob/master/LICENSE). Official logo was created by [Vic Shóstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/publicdomain/zero/1.0/) license (CC0 1.0 Universal).

View File

@ -1,6 +1,7 @@
package main
import (
"bytes"
"crypto/tls"
"flag"
"fmt"
@ -11,7 +12,6 @@ import (
"strings"
"time"
"github.com/go-redis/redis/v8"
"github.com/hibiken/asynq"
"github.com/hibiken/asynq/x/metrics"
"github.com/hibiken/asynqmon"
@ -20,100 +20,139 @@ import (
"github.com/rs/cors"
)
// Command-line flags
var (
flagPort int
flagRedisAddr string
flagRedisDB int
flagRedisPassword string
flagRedisTLS string
flagRedisURL string
flagRedisInsecureTLS bool
flagRedisClusterNodes string
flagMaxPayloadLength int
flagMaxResultLength int
flagEnableMetricsExporter bool
flagPrometheusServerAddr string
)
// Config holds configurations for the program provided via the command line.
type Config struct {
// Server port
Port int
func init() {
flag.IntVar(&flagPort, "port", getEnvOrDefaultInt("PORT", 8080), "port number to use for web ui server")
flag.StringVar(&flagRedisAddr, "redis-addr", getEnvDefaultString("REDIS_ADDR", "127.0.0.1:6379"), "address of redis server to connect to")
flag.IntVar(&flagRedisDB, "redis-db", getEnvOrDefaultInt("REDIS_DB", 0), "redis database number")
flag.StringVar(&flagRedisPassword, "redis-password", getEnvDefaultString("REDIS_PASSWORD", ""), "password to use when connecting to redis server")
flag.StringVar(&flagRedisTLS, "redis-tls", getEnvDefaultString("REDIS_TLS", ""), "server name for TLS validation used when connecting to redis server")
flag.StringVar(&flagRedisURL, "redis-url", getEnvDefaultString("REDIS_URL", ""), "URL to redis server")
flag.BoolVar(&flagRedisInsecureTLS, "redis-insecure-tls", getEnvOrDefaultBool("REDIS_INSECURE_TLS", false), "disable TLS certificate host checks")
flag.StringVar(&flagRedisClusterNodes, "redis-cluster-nodes", getEnvDefaultString("REDIS_CLUSTER_NODES", ""), "comma separated list of host:port addresses of cluster nodes")
flag.IntVar(&flagMaxPayloadLength, "max-payload-length", getEnvOrDefaultInt("MAX_PAYLOAD_LENGTH", 200), "maximum number of utf8 characters printed in the payload cell in the Web UI")
flag.IntVar(&flagMaxResultLength, "max-result-length", getEnvOrDefaultInt("MAX_RESULT_LENGTH", 200), "maximum number of utf8 characters printed in the result cell in the Web UI")
flag.BoolVar(&flagEnableMetricsExporter, "enable-metrics-exporter", getEnvOrDefaultBool("ENABLE_METRICS_EXPORTER", false), "enable prometheus metrics exporter to expose queue metrics")
flag.StringVar(&flagPrometheusServerAddr, "prometheus-addr", getEnvDefaultString("PROMETHEUS_ADDR", ""), "address of prometheus server to query time series")
// Redis connection options
RedisAddr string
RedisDB int
RedisPassword string
RedisTLS string
RedisURL string
RedisInsecureTLS bool
RedisClusterNodes string
// UI related configs
ReadOnly bool
MaxPayloadLength int
MaxResultLength int
// Prometheus related configs
EnableMetricsExporter bool
PrometheusServerAddr string
// Args are the positional (non-flag) command line arguments
Args []string
}
// TODO: Write test and refactor this code.
// IDEA: https://eli.thegreenplace.net/2020/testing-flag-parsing-in-go-programs/
func getRedisOptionsFromFlags() (asynq.RedisConnOpt, error) {
var opts redis.UniversalOptions
// parseFlags parses the command-line arguments provided to the program.
// Typically, os.Args[0] is provided as 'progname' and os.args[1:] as 'args'.
// Returns the Config in case parsing succeeded, or an error. In any case, the
// output of the flag.Parse is returned in output.
//
// Reference: https://eli.thegreenplace.net/2020/testing-flag-parsing-in-go-programs/
func parseFlags(progname string, args []string) (cfg *Config, output string, err error) {
flags := flag.NewFlagSet(progname, flag.ContinueOnError)
var buf bytes.Buffer
flags.SetOutput(&buf)
if flagRedisClusterNodes != "" {
opts.Addrs = strings.Split(flagRedisClusterNodes, ",")
opts.Password = flagRedisPassword
} else {
if flagRedisURL != "" {
res, err := redis.ParseURL(flagRedisURL)
if err != nil {
return nil, err
}
opts.Addrs = append(opts.Addrs, res.Addr)
opts.DB = res.DB
opts.Password = res.Password
var conf Config
flags.IntVar(&conf.Port, "port", getEnvOrDefaultInt("PORT", 8080), "port number to use for web ui server")
flags.StringVar(&conf.RedisAddr, "redis-addr", getEnvDefaultString("REDIS_ADDR", "127.0.0.1:6379"), "address of redis server to connect to")
flags.IntVar(&conf.RedisDB, "redis-db", getEnvOrDefaultInt("REDIS_DB", 0), "redis database number")
flags.StringVar(&conf.RedisPassword, "redis-password", getEnvDefaultString("REDIS_PASSWORD", ""), "password to use when connecting to redis server")
flags.StringVar(&conf.RedisTLS, "redis-tls", getEnvDefaultString("REDIS_TLS", ""), "server name for TLS validation used when connecting to redis server")
flags.StringVar(&conf.RedisURL, "redis-url", getEnvDefaultString("REDIS_URL", ""), "URL to redis server")
flags.BoolVar(&conf.RedisInsecureTLS, "redis-insecure-tls", getEnvOrDefaultBool("REDIS_INSECURE_TLS", false), "disable TLS certificate host checks")
flags.StringVar(&conf.RedisClusterNodes, "redis-cluster-nodes", getEnvDefaultString("REDIS_CLUSTER_NODES", ""), "comma separated list of host:port addresses of cluster nodes")
flags.IntVar(&conf.MaxPayloadLength, "max-payload-length", getEnvOrDefaultInt("MAX_PAYLOAD_LENGTH", 200), "maximum number of utf8 characters printed in the payload cell in the Web UI")
flags.IntVar(&conf.MaxResultLength, "max-result-length", getEnvOrDefaultInt("MAX_RESULT_LENGTH", 200), "maximum number of utf8 characters printed in the result cell in the Web UI")
flags.BoolVar(&conf.EnableMetricsExporter, "enable-metrics-exporter", getEnvOrDefaultBool("ENABLE_METRICS_EXPORTER", false), "enable prometheus metrics exporter to expose queue metrics")
flags.StringVar(&conf.PrometheusServerAddr, "prometheus-addr", getEnvDefaultString("PROMETHEUS_ADDR", ""), "address of prometheus server to query time series")
flags.BoolVar(&conf.ReadOnly, "read-only", getEnvOrDefaultBool("READ_ONLY", false), "restrict to read-only mode")
} else {
opts.Addrs = []string{flagRedisAddr}
opts.DB = flagRedisDB
opts.Password = flagRedisPassword
}
err = flags.Parse(args)
if err != nil {
return nil, buf.String(), err
}
conf.Args = flags.Args()
return &conf, buf.String(), nil
}
if flagRedisTLS != "" {
opts.TLSConfig = &tls.Config{ServerName: flagRedisTLS}
func makeTLSConfig(cfg *Config) *tls.Config {
if cfg.RedisTLS == "" && !cfg.RedisInsecureTLS {
return nil
}
if flagRedisInsecureTLS {
if opts.TLSConfig == nil {
opts.TLSConfig = &tls.Config{}
}
opts.TLSConfig.InsecureSkipVerify = true
return &tls.Config{
ServerName: cfg.RedisTLS,
InsecureSkipVerify: cfg.RedisInsecureTLS,
}
}
if flagRedisClusterNodes != "" {
func makeRedisConnOpt(cfg *Config) (asynq.RedisConnOpt, error) {
// Connecting to redis-cluster
if len(cfg.RedisClusterNodes) > 0 {
return asynq.RedisClusterClientOpt{
Addrs: opts.Addrs,
Password: opts.Password,
TLSConfig: opts.TLSConfig,
Addrs: strings.Split(cfg.RedisClusterNodes, ","),
Password: cfg.RedisPassword,
TLSConfig: makeTLSConfig(cfg),
}, nil
}
return asynq.RedisClientOpt{
Addr: opts.Addrs[0],
DB: opts.DB,
Password: opts.Password,
TLSConfig: opts.TLSConfig,
}, nil
// Connecting to redis-sentinels
if strings.HasPrefix(cfg.RedisURL, "redis-sentinel") {
res, err := asynq.ParseRedisURI(cfg.RedisURL)
if err != nil {
return nil, err
}
connOpt := res.(asynq.RedisFailoverClientOpt) // safe to type-assert
connOpt.TLSConfig = makeTLSConfig(cfg)
return connOpt, nil
}
// Connecting to single redis server
var connOpt asynq.RedisClientOpt
if len(cfg.RedisURL) > 0 {
res, err := asynq.ParseRedisURI(cfg.RedisURL)
if err != nil {
return nil, err
}
connOpt = res.(asynq.RedisClientOpt) // safe to type-assert
} else {
connOpt.Addr = cfg.RedisAddr
connOpt.DB = cfg.RedisDB
connOpt.Password = cfg.RedisPassword
}
if connOpt.TLSConfig == nil {
connOpt.TLSConfig = makeTLSConfig(cfg)
}
return connOpt, nil
}
func main() {
flag.Parse()
cfg, output, err := parseFlags(os.Args[0], os.Args[1:])
if err == flag.ErrHelp {
fmt.Println(output)
os.Exit(2)
} else if err != nil {
fmt.Printf("error: %v\n", err)
fmt.Println(output)
os.Exit(1)
}
redisConnOpt, err := getRedisOptionsFromFlags()
redisConnOpt, err := makeRedisConnOpt(cfg)
if err != nil {
log.Fatal(err)
}
h := asynqmon.New(asynqmon.Options{
RedisConnOpt: redisConnOpt,
PayloadFormatter: asynqmon.PayloadFormatterFunc(formatPayload),
ResultFormatter: asynqmon.ResultFormatterFunc(formatResult),
PrometheusAddress: flagPrometheusServerAddr,
PayloadFormatter: asynqmon.PayloadFormatterFunc(payloadFormatterFunc(cfg)),
ResultFormatter: asynqmon.ResultFormatterFunc(resultFormatterFunc(cfg)),
PrometheusAddress: cfg.PrometheusServerAddr,
ReadOnly: cfg.ReadOnly,
})
defer h.Close()
@ -122,7 +161,7 @@ func main() {
})
mux := http.NewServeMux()
mux.Handle("/", c.Handler(h))
if flagEnableMetricsExporter {
if cfg.EnableMetricsExporter {
// Using NewPedanticRegistry here to test the implementation of Collectors and Metrics.
reg := prometheus.NewPedanticRegistry()
@ -139,23 +178,27 @@ func main() {
srv := &http.Server{
Handler: mux,
Addr: fmt.Sprintf(":%d", flagPort),
Addr: fmt.Sprintf(":%d", cfg.Port),
WriteTimeout: 10 * time.Second,
ReadTimeout: 10 * time.Second,
}
fmt.Printf("Asynq Monitoring WebUI server is listening on port %d\n", flagPort)
fmt.Printf("Asynq Monitoring WebUI server is listening on port %d\n", cfg.Port)
log.Fatal(srv.ListenAndServe())
}
func formatPayload(taskType string, payload []byte) string {
payloadStr := asynqmon.DefaultPayloadFormatter.FormatPayload(taskType, payload)
return truncate(payloadStr, flagMaxPayloadLength)
func payloadFormatterFunc(cfg *Config) func(string, []byte) string {
return func(taskType string, payload []byte) string {
payloadStr := asynqmon.DefaultPayloadFormatter.FormatPayload(taskType, payload)
return truncate(payloadStr, cfg.MaxPayloadLength)
}
}
func formatResult(taskType string, result []byte) string {
resultStr := asynqmon.DefaultResultFormatter.FormatResult(taskType, result)
return truncate(resultStr, flagMaxResultLength)
func resultFormatterFunc(cfg *Config) func(string, []byte) string {
return func(taskType string, result []byte) string {
resultStr := asynqmon.DefaultResultFormatter.FormatResult(taskType, result)
return truncate(resultStr, cfg.MaxResultLength)
}
}
// truncates string s to limit length (in utf8).

137
cmd/asynqmon/main_test.go Normal file
View File

@ -0,0 +1,137 @@
package main
import (
"crypto/tls"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hibiken/asynq"
)
func TestParseFlags(t *testing.T) {
tests := []struct {
args []string
want *Config
}{
{
args: []string{"--redis-addr", "localhost:6380", "--redis-db", "3"},
want: &Config{
RedisAddr: "localhost:6380",
RedisDB: 3,
// Default values
Port: 8080,
RedisPassword: "",
RedisTLS: "",
RedisURL: "",
RedisInsecureTLS: false,
RedisClusterNodes: "",
MaxPayloadLength: 200,
MaxResultLength: 200,
EnableMetricsExporter: false,
PrometheusServerAddr: "",
ReadOnly: false,
Args: []string{},
},
},
}
for _, tc := range tests {
t.Run(strings.Join(tc.args, " "), func(t *testing.T) {
cfg, output, err := parseFlags("asynqmon", tc.args)
if err != nil {
t.Errorf("parseFlags returned error: %v", err)
}
if output != "" {
t.Errorf("parseFlag returned output=%q, want empty", output)
}
if diff := cmp.Diff(tc.want, cfg); diff != "" {
t.Errorf("parseFlag returned Config %v, want %v; (-want,+got)\n%s", cfg, tc.want, diff)
}
})
}
}
func TestMakeRedisConnOpt(t *testing.T) {
var tests = []struct {
desc string
cfg *Config
want asynq.RedisConnOpt
}{
{
desc: "With address, db number and password",
cfg: &Config{
RedisAddr: "localhost:6380",
RedisDB: 1,
RedisPassword: "foo",
},
want: asynq.RedisClientOpt{
Addr: "localhost:6380",
DB: 1,
Password: "foo",
},
},
{
desc: "With TLS server name",
cfg: &Config{
RedisAddr: "localhost:6379",
RedisTLS: "foobar",
},
want: asynq.RedisClientOpt{
Addr: "localhost:6379",
TLSConfig: &tls.Config{ServerName: "foobar"},
},
},
{
desc: "With redis URL",
cfg: &Config{
RedisURL: "redis://:bar@localhost:6381/2",
},
want: asynq.RedisClientOpt{
Addr: "localhost:6381",
DB: 2,
Password: "bar",
},
},
{
desc: "With redis-sentinel URL",
cfg: &Config{
RedisURL: "redis-sentinel://:secretpassword@localhost:5000,localhost:5001,localhost:5002?master=mymaster",
},
want: asynq.RedisFailoverClientOpt{
MasterName: "mymaster",
SentinelAddrs: []string{
"localhost:5000", "localhost:5001", "localhost:5002"},
Password: "secretpassword", // FIXME: Shouldn't this be SentinelPassword instead?
},
},
{
desc: "With cluster nodes",
cfg: &Config{
RedisClusterNodes: "localhost:5000,localhost:5001,localhost:5002,localhost:5003,localhost:5004,localhost:5005",
},
want: asynq.RedisClusterClientOpt{
Addrs: []string{
"localhost:5000", "localhost:5001", "localhost:5002", "localhost:5003", "localhost:5004", "localhost:5005"},
},
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got, err := makeRedisConnOpt(tc.cfg)
if err != nil {
t.Fatalf("makeRedisConnOpt returned error: %v", err)
}
if diff := cmp.Diff(tc.want, got, cmpopts.IgnoreUnexported(tls.Config{})); diff != "" {
t.Errorf("diff found: want=%v, got=%v; (-want,+got)\n%s",
tc.want, got, diff)
}
})
}
}

View File

@ -82,13 +82,21 @@ type queueStateSnapshot struct {
MemoryUsage int64 `json:"memory_usage_bytes"`
// Total number of tasks in the queue.
Size int `json:"size"`
// Totoal number of groups in the queue.
Groups int `json:"groups"`
// Latency of the queue in milliseconds.
LatencyMillisec int64 `json:"latency_msec"`
// Latency duration string for display purpose.
DisplayLatency string `json:"display_latency"`
// Number of tasks in each state.
Active int `json:"active"`
Pending int `json:"pending"`
Scheduled int `json:"scheduled"`
Retry int `json:"retry"`
Archived int `json:"archived"`
Completed int `json:"completed"`
Active int `json:"active"`
Pending int `json:"pending"`
Aggregating int `json:"aggregating"`
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.
@ -103,22 +111,26 @@ type queueStateSnapshot struct {
Timestamp time.Time `json:"timestamp"`
}
func toQueueStateSnapshot(s *asynq.QueueInfo) *queueStateSnapshot {
func toQueueStateSnapshot(info *asynq.QueueInfo) *queueStateSnapshot {
return &queueStateSnapshot{
Queue: s.Queue,
MemoryUsage: s.MemoryUsage,
Size: s.Size,
Active: s.Active,
Pending: s.Pending,
Scheduled: s.Scheduled,
Retry: s.Retry,
Archived: s.Archived,
Completed: s.Completed,
Processed: s.Processed,
Succeeded: s.Processed - s.Failed,
Failed: s.Failed,
Paused: s.Paused,
Timestamp: s.Timestamp,
Queue: info.Queue,
MemoryUsage: info.MemoryUsage,
Size: info.Size,
Groups: info.Groups,
LatencyMillisec: info.Latency.Milliseconds(),
DisplayLatency: info.Latency.Round(10 * time.Millisecond).String(),
Active: info.Active,
Pending: info.Pending,
Aggregating: info.Aggregating,
Scheduled: info.Scheduled,
Retry: info.Retry,
Archived: info.Archived,
Completed: info.Completed,
Processed: info.Processed,
Succeeded: info.Processed - info.Failed,
Failed: info.Failed,
Paused: info.Paused,
Timestamp: info.Timestamp,
}
}
@ -247,6 +259,9 @@ type activeTask struct {
// Value is either time formatted in RFC3339 format, or "-" which indicates that
// the data is not available yet.
Deadline string `json:"deadline"`
// IsOrphaned indicates whether the task is left in active state with no worker processing it.
IsOrphaned bool `json:"is_orphaned"`
}
func toActiveTask(ti *asynq.TaskInfo, pf PayloadFormatter) *activeTask {
@ -259,7 +274,7 @@ func toActiveTask(ti *asynq.TaskInfo, pf PayloadFormatter) *activeTask {
Retried: ti.Retried,
LastError: ti.LastErr,
}
return &activeTask{baseTask: base}
return &activeTask{baseTask: base, IsOrphaned: ti.IsOrphaned}
}
func toActiveTasks(in []*asynq.TaskInfo, pf PayloadFormatter) []*activeTask {
@ -298,6 +313,35 @@ func toPendingTasks(in []*asynq.TaskInfo, pf PayloadFormatter) []*pendingTask {
return out
}
type aggregatingTask struct {
*baseTask
Group string `json:"group"`
}
func toAggregatingTask(ti *asynq.TaskInfo, pf PayloadFormatter) *aggregatingTask {
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 &aggregatingTask{
baseTask: base,
Group: ti.Group,
}
}
func toAggregatingTasks(in []*asynq.TaskInfo, pf PayloadFormatter) []*aggregatingTask {
out := make([]*aggregatingTask, len(in))
for i, ti := range in {
out[i] = toAggregatingTask(ti, pf)
}
return out
}
type scheduledTask struct {
*baseTask
NextProcessAt time.Time `json:"next_process_at"`
@ -419,6 +463,26 @@ func toCompletedTasks(in []*asynq.TaskInfo, pf PayloadFormatter, rf ResultFormat
return out
}
type groupInfo struct {
Group string `json:"group"`
Size int `json:"size"`
}
func toGroupInfos(in []*asynq.GroupInfo) []*groupInfo {
out := make([]*groupInfo, len(in))
for i, g := range in {
out[i] = toGroupInfo(g)
}
return out
}
func toGroupInfo(in *asynq.GroupInfo) *groupInfo {
return &groupInfo{
Group: in.Group,
Size: in.Size,
}
}
type schedulerEntry struct {
ID string `json:"id"`
Spec string `json:"spec"`

19
go.mod
View File

@ -3,15 +3,16 @@ module github.com/hibiken/asynqmon
go 1.16
require (
github.com/go-redis/redis/v8 v8.11.4
github.com/google/uuid v1.3.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.7
github.com/gorilla/mux v1.8.0
github.com/hibiken/asynq v0.20.0
github.com/hibiken/asynq/x v0.0.0-20211219150637-8dfabfccb3be // indirect
github.com/prometheus/client_golang v1.11.0 // indirect
github.com/hibiken/asynq v0.24.1
github.com/hibiken/asynq/x v0.0.0-20211219150637-8dfabfccb3be
github.com/prometheus/client_golang v1.11.1
github.com/redis/go-redis/v9 v9.0.4
github.com/rs/cors v1.7.0
github.com/spf13/cast v1.4.1 // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
google.golang.org/protobuf v1.27.1 // indirect
github.com/spf13/cast v1.5.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
)

81
go.sum
View File

@ -10,20 +10,26 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@ -32,7 +38,6 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-redis/redis/v8 v8.11.2/go.mod h1:DLomh7y2e3ggQXQLd1YgmvIfecPJoFl7WU5SOQ/r06M=
github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
@ -51,8 +56,9 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@ -60,20 +66,18 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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.19.0 h1:AoJhoivymyFhF92ZAmVzxd7jr0RM264HdgkbjPc+x+M=
github.com/hibiken/asynq v0.19.0/go.mod h1:tyc63ojaW8SJ5SBm8mvI4DDONsguP5HE85EEl4Qr5Ig=
github.com/hibiken/asynq v0.19.1 h1:Xo4arUse8zhjEYj8wUXgvUSGshMRsV1qgtrSGetgUtg=
github.com/hibiken/asynq v0.19.1/go.mod h1:tyc63ojaW8SJ5SBm8mvI4DDONsguP5HE85EEl4Qr5Ig=
github.com/hibiken/asynq v0.20.0 h1:8k+safARLw9zwW91Tk/lbMl2OYTXUIvAibqJWGF6vMM=
github.com/hibiken/asynq v0.20.0/go.mod h1:tyc63ojaW8SJ5SBm8mvI4DDONsguP5HE85EEl4Qr5Ig=
github.com/hibiken/asynq v0.24.1 h1:+5iIEAyA9K/lcSPvx3qoPtsKJeKI5u9aOIvUmSsazEw=
github.com/hibiken/asynq v0.24.1/go.mod h1:u5qVeSbrnfT+vtG5Mq8ZPzQu/BmCKMHvTGb91uy9Tts=
github.com/hibiken/asynq/x v0.0.0-20211219150637-8dfabfccb3be h1:89J7WrDuoqFaKoQjZwqPczQXgXZ71liWYM+z9a8sILs=
github.com/hibiken/asynq/x v0.0.0-20211219150637-8dfabfccb3be/go.mod h1:VmxwMfMKyb6gyv8xG0oOBMXIhquWKPx+zPtbVBd2Q1s=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@ -87,8 +91,12 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -98,28 +106,25 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.11.1 h1:+4eQaD7vAZ6DsfsxB15hbE0odUjGI5ARs9yskGu1v4s=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@ -134,28 +139,34 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
github.com/redis/go-redis/v9 v9.0.4 h1:FC82T+CHJ/Q/PdyLW++GeCO+Ol59Y4T7R4jbgjvktgc=
github.com/redis/go-redis/v9 v9.0.4/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -164,7 +175,9 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -179,7 +192,7 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -190,6 +203,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -208,21 +222,21 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M=
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@ -230,6 +244,7 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -253,24 +268,22 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

40
group_handlers.go Normal file
View File

@ -0,0 +1,40 @@
package asynqmon
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/hibiken/asynq"
)
type listGroupsResponse struct {
Queue *queueStateSnapshot `json:"stats"`
Groups []*groupInfo `json:"groups"`
}
func newListGroupsHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
qname := mux.Vars(r)["qname"]
groups, err := inspector.Groups(qname)
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
}
resp := listGroupsResponse{
Queue: toQueueStateSnapshot(qinfo),
Groups: toGroupInfos(groups),
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}

View File

@ -6,13 +6,12 @@ import (
"net/http"
"strings"
"github.com/go-redis/redis/v8"
"github.com/gorilla/mux"
"github.com/hibiken/asynq"
"github.com/redis/go-redis/v9"
)
// Options is used to configure HTTPHandler.
// Options are used to configure HTTPHandler.
type Options struct {
// URL path the handler is responsible for.
// The path is used for the homepage of asynqmon, and every other page is rooted in this subtree.
@ -40,6 +39,9 @@ type Options struct {
// This field is optional. If this field is set, asynqmon will query the Prometheus server
// to get the time series data about queue metrics and show them in the web UI.
PrometheusAddress string
// Set ReadOnly to true to restrict user to view-only mode.
ReadOnly bool
}
// HTTPHandler is a http.Handler for asynqmon application.
@ -111,6 +113,7 @@ func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspecto
}
api := router.PathPrefix("/api").Subrouter()
// Queue endpoints.
api.HandleFunc("/queues", newListQueuesHandlerFunc(inspector)).Methods("GET")
api.HandleFunc("/queues/{qname}", newGetQueueHandlerFunc(inspector)).Methods("GET")
@ -170,8 +173,22 @@ func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspecto
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}/groups/{gname}/aggregating_tasks", newListAggregatingTasksHandlerFunc(inspector, payloadFmt)).Methods("GET")
api.HandleFunc("/queues/{qname}/groups/{gname}/aggregating_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE")
api.HandleFunc("/queues/{qname}/groups/{gname}/aggregating_tasks:delete_all", newDeleteAllAggregatingTasksHandlerFunc(inspector)).Methods("DELETE")
api.HandleFunc("/queues/{qname}/groups/{gname}/aggregating_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST")
api.HandleFunc("/queues/{qname}/groups/{gname}/aggregating_tasks/{task_id}:run", newRunTaskHandlerFunc(inspector)).Methods("POST")
api.HandleFunc("/queues/{qname}/groups/{gname}/aggregating_tasks:run_all", newRunAllAggregatingTasksHandlerFunc(inspector)).Methods("POST")
api.HandleFunc("/queues/{qname}/groups/{gname}/aggregating_tasks:batch_run", newBatchRunTasksHandlerFunc(inspector)).Methods("POST")
api.HandleFunc("/queues/{qname}/groups/{gname}/aggregating_tasks/{task_id}:archive", newArchiveTaskHandlerFunc(inspector)).Methods("POST")
api.HandleFunc("/queues/{qname}/groups/{gname}/aggregating_tasks:archive_all", newArchiveAllAggregatingTasksHandlerFunc(inspector)).Methods("POST")
api.HandleFunc("/queues/{qname}/groups/{gname}/aggregating_tasks:batch_archive", newBatchArchiveTasksHandlerFunc(inspector)).Methods("POST")
api.HandleFunc("/queues/{qname}/tasks/{task_id}", newGetTaskHandlerFunc(inspector, payloadFmt, resultFmt)).Methods("GET")
// Groups endponts
api.HandleFunc("/queues/{qname}/groups", newListGroupsHandlerFunc(inspector)).Methods("GET")
// Servers endpoints.
api.HandleFunc("/servers", newListServersHandlerFunc(inspector, payloadFmt)).Methods("GET")
@ -190,6 +207,11 @@ func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspecto
// Time series metrics endpoints.
api.HandleFunc("/metrics", newGetMetricsHandlerFunc(http.DefaultClient, opts.PrometheusAddress)).Methods("GET")
// Restrict APIs when running in read-only mode.
if opts.ReadOnly {
api.Use(restrictToReadOnly)
}
// Everything else, route to uiAssetsHandler.
router.NotFoundHandler = &uiAssetsHandler{
rootPath: opts.RootPath,
@ -197,6 +219,19 @@ func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspecto
staticDirPath: "ui/build",
indexFileName: "index.html",
prometheusAddr: opts.PrometheusAddress,
readOnly: opts.ReadOnly,
}
return router
}
// restrictToReadOnly is a middleware function to restrict users to perform only GET requests.
func restrictToReadOnly(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" && r.Method != "" {
http.Error(w, fmt.Sprintf("API Server is running in read-only mode: %s request is not allowed", r.Method), http.StatusMethodNotAllowed)
return
}
h.ServeHTTP(w, r)
})
}

View File

@ -6,9 +6,8 @@ import (
"net/http"
"strings"
"github.com/go-redis/redis/v8"
"github.com/hibiken/asynq"
"github.com/redis/go-redis/v9"
)
// ****************************************************************************

View File

@ -20,6 +20,7 @@ type uiAssetsHandler struct {
staticDirPath string
indexFileName string
prometheusAddr string
readOnly bool
}
// ServeHTTP inspects the URL path to locate a file within the static dir
@ -62,9 +63,11 @@ func (h *uiAssetsHandler) renderIndexFile(w http.ResponseWriter) error {
data := struct {
RootPath string
PrometheusAddr string
ReadOnly bool
}{
RootPath: h.rootPath,
PrometheusAddr: h.prometheusAddr,
ReadOnly: h.readOnly,
}
return tmpl.Execute(w, data)
}
@ -95,6 +98,14 @@ func (h *uiAssetsHandler) serveFile(w http.ResponseWriter, path string) (code in
}
return http.StatusInternalServerError, err
}
// Setting the MIME type for .js files manually to application/javascript as
// http.DetectContentType is using https://mimesniff.spec.whatwg.org/ which
// will not recognize application/javascript for security reasons.
if strings.HasSuffix(path, ".js") {
w.Header().Add("Content-Type", "application/javascript; charset=utf-8")
} else {
w.Header().Add("Content-Type", http.DetectContentType(bytes))
}
if _, err := w.Write(bytes); err != nil {
return http.StatusInternalServerError, err

View File

@ -71,10 +71,7 @@ func newListActiveTasksHandlerFunc(inspector *asynq.Inspector, pf PayloadFormatt
Tasks: activeTasks,
Stats: toQueueStateSnapshot(qinfo),
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeResponseJSON(w, resp)
}
}
@ -149,10 +146,7 @@ func newBatchCancelActiveTasksHandlerFunc(inspector *asynq.Inspector) http.Handl
resp.CanceledIDs = append(resp.CanceledIDs, id)
}
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeResponseJSON(w, resp)
}
}
@ -180,10 +174,7 @@ func newListPendingTasksHandlerFunc(inspector *asynq.Inspector, pf PayloadFormat
payload["tasks"] = toPendingTasks(tasks, pf)
}
payload["stats"] = toQueueStateSnapshot(qinfo)
if err := json.NewEncoder(w).Encode(payload); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeResponseJSON(w, payload)
}
}
@ -211,10 +202,7 @@ func newListScheduledTasksHandlerFunc(inspector *asynq.Inspector, pf PayloadForm
payload["tasks"] = toScheduledTasks(tasks, pf)
}
payload["stats"] = toQueueStateSnapshot(qinfo)
if err := json.NewEncoder(w).Encode(payload); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeResponseJSON(w, payload)
}
}
@ -242,10 +230,7 @@ func newListRetryTasksHandlerFunc(inspector *asynq.Inspector, pf PayloadFormatte
payload["tasks"] = toRetryTasks(tasks, pf)
}
payload["stats"] = toQueueStateSnapshot(qinfo)
if err := json.NewEncoder(w).Encode(payload); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeResponseJSON(w, payload)
}
}
@ -273,10 +258,7 @@ func newListArchivedTasksHandlerFunc(inspector *asynq.Inspector, pf PayloadForma
payload["tasks"] = toArchivedTasks(tasks, pf)
}
payload["stats"] = toQueueStateSnapshot(qinfo)
if err := json.NewEncoder(w).Encode(payload); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeResponseJSON(w, payload)
}
}
@ -303,10 +285,42 @@ func newListCompletedTasksHandlerFunc(inspector *asynq.Inspector, pf PayloadForm
payload["tasks"] = toCompletedTasks(tasks, pf, rf)
}
payload["stats"] = toQueueStateSnapshot(qinfo)
if err := json.NewEncoder(w).Encode(payload); err != nil {
writeResponseJSON(w, payload)
}
}
func newListAggregatingTasksHandlerFunc(inspector *asynq.Inspector, pf PayloadFormatter) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
qname := vars["qname"]
gname := vars["gname"]
pageSize, pageNum := getPageOptions(r)
tasks, err := inspector.ListAggregatingTasks(
qname, gname, 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
}
groups, err := inspector.Groups(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([]*aggregatingTask, 0)
} else {
payload["tasks"] = toAggregatingTasks(tasks, pf)
}
payload["stats"] = toQueueStateSnapshot(qinfo)
payload["groups"] = toGroupInfos(groups)
writeResponseJSON(w, payload)
}
}
@ -374,11 +388,20 @@ func newDeleteAllPendingTasksHandlerFunc(inspector *asynq.Inspector) http.Handle
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp := deleteAllTasksResponse{n}
if err := json.NewEncoder(w).Encode(resp); err != nil {
writeResponseJSON(w, deleteAllTasksResponse{n})
}
}
func newDeleteAllAggregatingTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
qname, gname := vars["qname"], vars["gname"]
n, err := inspector.DeleteAllAggregatingTasks(qname, gname)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeResponseJSON(w, deleteAllTasksResponse{n})
}
}
@ -390,11 +413,7 @@ func newDeleteAllScheduledTasksHandlerFunc(inspector *asynq.Inspector) http.Hand
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
}
writeResponseJSON(w, deleteAllTasksResponse{n})
}
}
@ -406,11 +425,7 @@ func newDeleteAllRetryTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerF
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
}
writeResponseJSON(w, deleteAllTasksResponse{n})
}
}
@ -422,11 +437,7 @@ func newDeleteAllArchivedTasksHandlerFunc(inspector *asynq.Inspector) http.Handl
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
}
writeResponseJSON(w, deleteAllTasksResponse{n})
}
}
@ -438,77 +449,121 @@ func newDeleteAllCompletedTasksHandlerFunc(inspector *asynq.Inspector) http.Hand
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
}
writeResponseJSON(w, deleteAllTasksResponse{n})
}
}
type runAllTasksResponse struct {
// Number of tasks scheduled to run.
Scheduled int `json:"scheduled"`
}
func newRunAllScheduledTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
qname := mux.Vars(r)["qname"]
if _, err := inspector.RunAllScheduledTasks(qname); err != nil {
n, err := inspector.RunAllScheduledTasks(qname)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
writeResponseJSON(w, runAllTasksResponse{n})
}
}
func newRunAllRetryTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
qname := mux.Vars(r)["qname"]
if _, err := inspector.RunAllRetryTasks(qname); err != nil {
n, err := inspector.RunAllRetryTasks(qname)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
writeResponseJSON(w, runAllTasksResponse{n})
}
}
func newRunAllArchivedTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
qname := mux.Vars(r)["qname"]
if _, err := inspector.RunAllArchivedTasks(qname); err != nil {
n, err := inspector.RunAllArchivedTasks(qname)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
writeResponseJSON(w, runAllTasksResponse{n})
}
}
func newRunAllAggregatingTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
qname, gname := vars["qname"], vars["gname"]
n, err := inspector.RunAllAggregatingTasks(qname, gname)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeResponseJSON(w, runAllTasksResponse{n})
}
}
type archiveAllTasksResponse struct {
// Number of tasks archived.
Archived int `json:"archived"`
}
func writeResponseJSON(w http.ResponseWriter, resp interface{}) {
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func newArchiveAllPendingTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
qname := mux.Vars(r)["qname"]
if _, err := inspector.ArchiveAllPendingTasks(qname); err != nil {
n, err := inspector.ArchiveAllPendingTasks(qname)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
writeResponseJSON(w, archiveAllTasksResponse{n})
}
}
func newArchiveAllAggregatingTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
qname, gname := vars["qname"], vars["gname"]
n, err := inspector.ArchiveAllAggregatingTasks(qname, gname)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeResponseJSON(w, archiveAllTasksResponse{n})
}
}
func newArchiveAllScheduledTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
qname := mux.Vars(r)["qname"]
if _, err := inspector.ArchiveAllScheduledTasks(qname); err != nil {
n, err := inspector.ArchiveAllScheduledTasks(qname)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
writeResponseJSON(w, archiveAllTasksResponse{n})
}
}
func newArchiveAllRetryTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
qname := mux.Vars(r)["qname"]
if _, err := inspector.ArchiveAllRetryTasks(qname); err != nil {
n, err := inspector.ArchiveAllRetryTasks(qname)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
writeResponseJSON(w, archiveAllTasksResponse{n})
}
}
@ -559,10 +614,7 @@ func newBatchDeleteTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc
resp.DeletedIDs = append(resp.DeletedIDs, taskid)
}
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeResponseJSON(w, resp)
}
}
@ -603,10 +655,7 @@ func newBatchRunTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
resp.PendingIDs = append(resp.PendingIDs, taskid)
}
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeResponseJSON(w, resp)
}
}
@ -647,10 +696,7 @@ func newBatchArchiveTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFun
resp.ArchivedIDs = append(resp.ArchivedIDs, taskid)
}
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeResponseJSON(w, resp)
}
}
@ -696,9 +742,6 @@ func newGetTaskHandlerFunc(inspector *asynq.Inspector, pf PayloadFormatter, rf R
return
}
if err := json.NewEncoder(w).Encode(toTaskInfo(info, pf, rf)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeResponseJSON(w, toTaskInfo(info, pf, rf))
}
}

View File

@ -1,17 +1,19 @@
{
"files": {
"main.js": "/[[.RootPath]]/static/js/main.c4fad8d1.chunk.js",
"main.js.map": "/[[.RootPath]]/static/js/main.c4fad8d1.chunk.js.map",
"main.js": "/[[.RootPath]]/static/js/main.5adda2da.chunk.js",
"main.js.map": "/[[.RootPath]]/static/js/main.5adda2da.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.60672392.chunk.js": "/[[.RootPath]]/static/js/2.60672392.chunk.js",
"static/js/2.60672392.chunk.js.map": "/[[.RootPath]]/static/js/2.60672392.chunk.js.map",
"static/js/2.83624df2.chunk.js": "/[[.RootPath]]/static/js/2.83624df2.chunk.js",
"static/js/2.83624df2.chunk.js.map": "/[[.RootPath]]/static/js/2.83624df2.chunk.js.map",
"index.html": "/[[.RootPath]]/index.html",
"static/js/2.60672392.chunk.js.LICENSE.txt": "/[[.RootPath]]/static/js/2.60672392.chunk.js.LICENSE.txt"
"static/js/2.83624df2.chunk.js.LICENSE.txt": "/[[.RootPath]]/static/js/2.83624df2.chunk.js.LICENSE.txt",
"static/media/logo-color.c2b0c1f3.svg": "/[[.RootPath]]/static/media/logo-color.c2b0c1f3.svg",
"static/media/logo-white.3fa2ac55.svg": "/[[.RootPath]]/static/media/logo-white.3fa2ac55.svg"
},
"entrypoints": [
"static/js/runtime-main.9fea6c1a.js",
"static/js/2.60672392.chunk.js",
"static/js/main.c4fad8d1.chunk.js"
"static/js/2.83624df2.chunk.js",
"static/js/main.5adda2da.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]]",window.PROMETHEUS_SERVER_ADDRESS="/[[.PrometheusAddr]]"</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.60672392.chunk.js"></script><script src="/[[.RootPath]]/static/js/main.c4fad8d1.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.FLAG_ROOT_PATH="/[[.RootPath]]",window.FLAG_PROMETHEUS_SERVER_ADDRESS="/[[.PrometheusAddr]]",window.FLAG_READ_ONLY="/[[.ReadOnly]]"</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.83624df2.chunk.js"></script><script src="/[[.RootPath]]/static/js/main.5adda2da.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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -28,8 +28,9 @@
"react-dom": "^16.13.1",
"react-redux": "7.2.4",
"react-router-dom": "5.3.0",
"react-scripts": "4.0.3",
"react-scripts": "5.0.1",
"react-syntax-highlighter": "15.4.3",
"react-window": "1.8.6",
"recharts": "2.1.4",
"typescript": "~4.2.4"
},
@ -55,6 +56,7 @@
]
},
"devDependencies": {
"@types/react-window": "1.8.5",
"redux-devtools": "3.7.0"
},
"homepage": "/[[.RootPath]]"

View File

@ -50,8 +50,9 @@
href="https://fonts.googleapis.com/icon?family=Material+Icons"
/>
<script>
window.ROOT_PATH = "%PUBLIC_URL%";
window.PROMETHEUS_SERVER_ADDRESS = "/[[.PrometheusAddr]]";
window.FLAG_ROOT_PATH = "%PUBLIC_URL%";
window.FLAG_PROMETHEUS_SERVER_ADDRESS = "/[[.PrometheusAddr]]";
window.FLAG_READ_ONLY = "/[[.ReadOnly]]";
</script>
<title>Asynq - Monitoring</title>
</head>

View File

@ -12,7 +12,6 @@ import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import Snackbar from "@material-ui/core/Snackbar";
import SnackbarContent from "@material-ui/core/SnackbarContent";
import Typography from "@material-ui/core/Typography";
import IconButton from "@material-ui/core/IconButton";
import Slide from "@material-ui/core/Slide";
import { TransitionProps } from "@material-ui/core/transitions";
@ -26,7 +25,7 @@ import TimelineIcon from "@material-ui/icons/Timeline";
import DoubleArrowIcon from "@material-ui/icons/DoubleArrow";
import CloseIcon from "@material-ui/icons/Close";
import { AppState } from "./store";
import { paths } from "./paths";
import { paths as getPaths } from "./paths";
import { isDarkTheme, useTheme } from "./theme";
import { closeSnackbar } from "./actions/snackbarActions";
import { toggleDrawer } from "./actions/settingsActions";
@ -40,6 +39,8 @@ import ServersView from "./views/ServersView";
import RedisInfoView from "./views/RedisInfoView";
import MetricsView from "./views/MetricsView";
import PageNotFoundView from "./views/PageNotFoundView";
import { ReactComponent as Logo } from "./images/logo-color.svg";
import { ReactComponent as LogoDarkTheme } from "./images/logo-white.svg";
const drawerWidth = 220;
@ -74,9 +75,6 @@ const useStyles = (theme: Theme) =>
menuButtonHidden: {
display: "none",
},
title: {
flexGrow: 1,
},
drawerPaper: {
position: "relative",
whiteSpace: "nowrap",
@ -156,6 +154,7 @@ function SlideUpTransition(props: TransitionProps) {
function App(props: ConnectedProps<typeof connector>) {
const theme = useTheme(props.themePreference);
const classes = useStyles(theme)();
const paths = getPaths();
return (
<ThemeProvider theme={theme}>
<Router>
@ -175,15 +174,11 @@ function App(props: ConnectedProps<typeof connector>) {
>
<MenuIcon />
</IconButton>
<Typography
component="h1"
variant="h6"
noWrap
className={classes.title}
color="textPrimary"
>
Asynq Monitoring
</Typography>
{isDarkTheme(theme) ? (
<LogoDarkTheme width={200} height={48} />
) : (
<Logo width={200} height={48} />
)}
</Toolbar>
</AppBar>
<div className={classes.mainContainer}>

View File

@ -0,0 +1,52 @@
import { Dispatch } from "redux";
import { listGroups, ListGroupsResponse } from "../api";
import { toErrorString, toErrorStringWithHttpStatus } from "../utils";
// List of groups related action types.
export const LIST_GROUPS_BEGIN = "LIST_GROUPS_BEGIN";
export const LIST_GROUPS_SUCCESS = "LIST_GROUPS_SUCCESS";
export const LIST_GROUPS_ERROR = "LIST_GROUPS_ERROR";
interface ListGroupsBeginAction {
type: typeof LIST_GROUPS_BEGIN;
queue: string;
}
interface ListGroupsSuccessAction {
type: typeof LIST_GROUPS_SUCCESS;
payload: ListGroupsResponse;
queue: string;
}
interface ListGroupsErrorAction {
type: typeof LIST_GROUPS_ERROR;
queue: string;
error: string;
}
// Union of all groups related action types.
export type GroupsActionTypes =
| ListGroupsBeginAction
| ListGroupsSuccessAction
| ListGroupsErrorAction;
export function listGroupsAsync(qname: string) {
return async (dispatch: Dispatch<GroupsActionTypes>) => {
dispatch({ type: LIST_GROUPS_BEGIN, queue: qname });
try {
const response = await listGroups(qname);
dispatch({
type: LIST_GROUPS_SUCCESS,
payload: response,
queue: qname,
});
} catch (error) {
console.error(`listGroupsAsync: ${toErrorStringWithHttpStatus(error)}`);
dispatch({
type: LIST_GROUPS_ERROR,
error: toErrorString(error),
queue: qname,
});
}
};
}

View File

@ -34,6 +34,7 @@ import {
listRetryTasks,
listScheduledTasks,
listCompletedTasks,
listAggregatingTasks,
PaginationOptions,
runAllArchivedTasks,
runAllRetryTasks,
@ -49,6 +50,16 @@ import {
archiveAllPendingTasks,
TaskInfo,
getTaskInfo,
deleteAllAggregatingTasks,
archiveAllAggregatingTasks,
runAllAggregatingTasks,
batchDeleteAggregatingTasks,
batchArchiveAggregatingTasks,
batchRunAggregatingTasks,
deleteAggregatingTask,
runAggregatingTask,
archiveAggregatingTask,
ListAggregatingTasksResponse,
} from "../api";
import { Dispatch } from "redux";
import { toErrorString, toErrorStringWithHttpStatus } from "../utils";
@ -75,6 +86,9 @@ 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 LIST_AGGREGATING_TASKS_BEGIN = "LIST_AGGREGATING_TASKS_BEGIN";
export const LIST_AGGREGATING_TASKS_SUCCESS = "LIST_AGGREGATING_TASKS_SUCCESS";
export const LIST_AGGREGATING_TASKS_ERROR = "LIST_AGGREGATING_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";
@ -112,6 +126,17 @@ export const ARCHIVE_SCHEDULED_TASK_ERROR = "ARCHIVE_SCHEDULED_TASK_ERROR";
export const ARCHIVE_RETRY_TASK_BEGIN = "ARCHIVE_RETRY_TASK_BEGIN";
export const ARCHIVE_RETRY_TASK_SUCCESS = "ARCHIVE_RETRY_TASK_SUCCESS";
export const ARCHIVE_RETRY_TASK_ERROR = "ARCHIVE_RETRY_TASK_ERROR";
export const RUN_AGGREGATING_TASK_BEGIN = "RUN_AGGREGATING_TASK_BEGIN";
export const RUN_AGGREGATING_TASK_SUCCESS = "RUN_AGGREGATING_TASK_SUCCESS";
export const RUN_AGGREGATING_TASK_ERROR = "RUN_AGGREGATING_TASK_ERROR";
export const DELETE_AGGREGATING_TASK_BEGIN = "DELETE_AGGREGATING_TASK_BEGIN";
export const DELETE_AGGREGATING_TASK_SUCCESS =
"DELETE_AGGREGATING_TASK_SUCCESS";
export const DELETE_AGGREGATING_TASK_ERROR = "DELETE_AGGREGATING_TASK_ERROR";
export const ARCHIVE_AGGREGATING_TASK_BEGIN = "ARCHIVE_AGGREGATING_TASK_BEGIN";
export const ARCHIVE_AGGREGATING_TASK_SUCCESS =
"ARCHIVE_AGGREGATING_TASK_SUCCESS";
export const ARCHIVE_AGGREGATING_TASK_ERROR = "ARCHIVE_AGGREGATING_TASK_ERROR";
export const BATCH_ARCHIVE_PENDING_TASKS_BEGIN =
"BATCH_ARCHIVE_PENDING_TASKS_BEGIN";
export const BATCH_ARCHIVE_PENDING_TASKS_SUCCESS =
@ -231,6 +256,42 @@ export const BATCH_DELETE_COMPLETED_TASKS_SUCCESS =
"BATCH_DELETE_COMPLETED_TASKS_SUCCESS";
export const BATCH_DELETE_COMPLETED_TASKS_ERROR =
"BATCH_DELETE_COMPLETED_TASKS_ERROR";
export const BATCH_RUN_AGGREGATING_TASKS_BEGIN =
"BATCH_RUN_AGGREGATING_TASKS_BEGIN";
export const BATCH_RUN_AGGREGATING_TASKS_SUCCESS =
"BATCH_RUN_AGGREGATING_TASKS_SUCCESS";
export const BATCH_RUN_AGGREGATING_TASKS_ERROR =
"BATCH_RUN_AGGREGATING_TASKS_ERROR";
export const BATCH_ARCHIVE_AGGREGATING_TASKS_BEGIN =
"BATCH_ARCHIVE_AGGREGATING_TASKS_BEGIN";
export const BATCH_ARCHIVE_AGGREGATING_TASKS_SUCCESS =
"BATCH_ARCHIVE_AGGREGATING_TASKS_SUCCESS";
export const BATCH_ARCHIVE_AGGREGATING_TASKS_ERROR =
"BATCH_RUN_AGGREGATING_TASKS_ERROR";
export const BATCH_DELETE_AGGREGATING_TASKS_BEGIN =
"BATCH_DELETE_AGGREGATING_TASKS_BEGIN";
export const BATCH_DELETE_AGGREGATING_TASKS_SUCCESS =
"BATCH_DELETE_AGGREGATING_TASKS_SUCCESS";
export const BATCH_DELETE_AGGREGATING_TASKS_ERROR =
"BATCH_DELETE_AGGREGATING_TASKS_ERROR";
export const RUN_ALL_AGGREGATING_TASKS_BEGIN =
"RUN_ALL_AGGREGATING_TASKS_BEGIN";
export const RUN_ALL_AGGREGATING_TASKS_SUCCESS =
"RUN_ALL_AGGREGATING_TASKS_SUCCESS";
export const RUN_ALL_AGGREGATING_TASKS_ERROR =
"RUN_ALL_AGGREGATING_TASKS_ERROR";
export const ARCHIVE_ALL_AGGREGATING_TASKS_BEGIN =
"ARCHIVE_ALL_AGGREGATING_TASKS_BEGIN";
export const ARCHIVE_ALL_AGGREGATING_TASKS_SUCCESS =
"ARCHIVE_ALL_AGGREGATING_TASKS_SUCCESS";
export const ARCHIVE_ALL_AGGREGATING_TASKS_ERROR =
"ARCHIVE_ALL_AGGREGATING_TASKS_ERROR";
export const DELETE_ALL_AGGREGATING_TASKS_BEGIN =
"DELETE_ALL_AGGREGATING_TASKS_BEGIN";
export const DELETE_ALL_AGGREGATING_TASKS_SUCCESS =
"DELETE_ALL_AGGREGATING_TASKS_SUCCESS";
export const DELETE_ALL_AGGREGATING_TASKS_ERROR =
"DELETE_ALL_AGGREGATING_TASKS_ERROR";
interface GetTaskInfoBeginAction {
type: typeof GET_TASK_INFO_BEGIN;
@ -348,6 +409,26 @@ interface ListCompletedTasksErrorAction {
error: string; // error description
}
interface ListAggregatingTasksBeginAction {
type: typeof LIST_AGGREGATING_TASKS_BEGIN;
queue: string;
group: string;
}
interface ListAggregatingTasksSuccessAction {
type: typeof LIST_AGGREGATING_TASKS_SUCCESS;
queue: string;
group: string;
payload: ListAggregatingTasksResponse;
}
interface ListAggregatingTasksErrorAction {
type: typeof LIST_AGGREGATING_TASKS_ERROR;
queue: string;
group: string;
error: string; // error description
}
interface CancelActiveTaskBeginAction {
type: typeof CANCEL_ACTIVE_TASK_BEGIN;
queue: string;
@ -1001,6 +1082,189 @@ interface DeleteAllCompletedTasksErrorAction {
error: string;
}
interface DeleteAggregatingTaskBeginAction {
type: typeof DELETE_AGGREGATING_TASK_BEGIN;
queue: string;
taskId: string;
}
interface DeleteAggregatingTaskSuccessAction {
type: typeof DELETE_AGGREGATING_TASK_SUCCESS;
queue: string;
taskId: string;
}
interface DeleteAggregatingTaskErrorAction {
type: typeof DELETE_AGGREGATING_TASK_ERROR;
queue: string;
taskId: string;
error: string;
}
interface RunAggregatingTaskBeginAction {
type: typeof RUN_AGGREGATING_TASK_BEGIN;
queue: string;
taskId: string;
}
interface RunAggregatingTaskSuccessAction {
type: typeof RUN_AGGREGATING_TASK_SUCCESS;
queue: string;
taskId: string;
}
interface RunAggregatingTaskErrorAction {
type: typeof RUN_AGGREGATING_TASK_ERROR;
queue: string;
taskId: string;
error: string;
}
interface ArchiveAggregatingTaskBeginAction {
type: typeof ARCHIVE_AGGREGATING_TASK_BEGIN;
queue: string;
taskId: string;
}
interface ArchiveAggregatingTaskSuccessAction {
type: typeof ARCHIVE_AGGREGATING_TASK_SUCCESS;
queue: string;
taskId: string;
}
interface ArchiveAggregatingTaskErrorAction {
type: typeof ARCHIVE_AGGREGATING_TASK_ERROR;
queue: string;
taskId: string;
error: string;
}
interface BatchDeleteAggregatingTasksBeginAction {
type: typeof BATCH_DELETE_AGGREGATING_TASKS_BEGIN;
queue: string;
group: string;
taskIds: string[];
}
interface BatchDeleteAggregatingTasksSuccessAction {
type: typeof BATCH_DELETE_AGGREGATING_TASKS_SUCCESS;
queue: string;
group: string;
payload: BatchDeleteTasksResponse;
}
interface BatchDeleteAggregatingTasksErrorAction {
type: typeof BATCH_DELETE_AGGREGATING_TASKS_ERROR;
queue: string;
group: string;
taskIds: string[];
error: string;
}
interface BatchRunAggregatingTasksBeginAction {
type: typeof BATCH_RUN_AGGREGATING_TASKS_BEGIN;
queue: string;
group: string;
taskIds: string[];
}
interface BatchRunAggregatingTasksSuccessAction {
type: typeof BATCH_RUN_AGGREGATING_TASKS_SUCCESS;
queue: string;
group: string;
payload: BatchRunTasksResponse;
}
interface BatchRunAggregatingTasksErrorAction {
type: typeof BATCH_RUN_AGGREGATING_TASKS_ERROR;
queue: string;
group: string;
taskIds: string[];
error: string;
}
interface RunAllAggregatingTasksBeginAction {
type: typeof RUN_ALL_AGGREGATING_TASKS_BEGIN;
queue: string;
group: string;
}
interface RunAllAggregatingTasksSuccessAction {
type: typeof RUN_ALL_AGGREGATING_TASKS_SUCCESS;
scheduled: number;
queue: string;
group: string;
}
interface RunAllAggregatingTasksErrorAction {
type: typeof RUN_ALL_AGGREGATING_TASKS_ERROR;
queue: string;
group: string;
error: string;
}
interface BatchArchiveAggregatingTasksBeginAction {
type: typeof BATCH_ARCHIVE_AGGREGATING_TASKS_BEGIN;
queue: string;
group: string;
taskIds: string[];
}
interface BatchArchiveAggregatingTasksSuccessAction {
type: typeof BATCH_ARCHIVE_AGGREGATING_TASKS_SUCCESS;
queue: string;
group: string;
payload: BatchArchiveTasksResponse;
}
interface BatchArchiveAggregatingTasksErrorAction {
type: typeof BATCH_ARCHIVE_AGGREGATING_TASKS_ERROR;
queue: string;
group: string;
taskIds: string[];
error: string;
}
interface ArchiveAllAggregatingTasksBeginAction {
type: typeof ARCHIVE_ALL_AGGREGATING_TASKS_BEGIN;
queue: string;
group: string;
}
interface ArchiveAllAggregatingTasksSuccessAction {
type: typeof ARCHIVE_ALL_AGGREGATING_TASKS_SUCCESS;
queue: string;
group: string;
archived: number;
}
interface ArchiveAllAggregatingTasksErrorAction {
type: typeof ARCHIVE_ALL_AGGREGATING_TASKS_ERROR;
queue: string;
group: string;
error: string;
}
interface DeleteAllAggregatingTasksBeginAction {
type: typeof DELETE_ALL_AGGREGATING_TASKS_BEGIN;
queue: string;
group: string;
}
interface DeleteAllAggregatingTasksSuccessAction {
type: typeof DELETE_ALL_AGGREGATING_TASKS_SUCCESS;
queue: string;
group: string;
deleted: number;
}
interface DeleteAllAggregatingTasksErrorAction {
type: typeof DELETE_ALL_AGGREGATING_TASKS_ERROR;
queue: string;
group: string;
error: string;
}
// Union of all tasks related action types.
export type TasksActionTypes =
| GetTaskInfoBeginAction
@ -1024,6 +1288,9 @@ export type TasksActionTypes =
| ListCompletedTasksBeginAction
| ListCompletedTasksSuccessAction
| ListCompletedTasksErrorAction
| ListAggregatingTasksBeginAction
| ListAggregatingTasksSuccessAction
| ListAggregatingTasksErrorAction
| CancelActiveTaskBeginAction
| CancelActiveTaskSuccessAction
| CancelActiveTaskErrorAction
@ -1131,7 +1398,34 @@ export type TasksActionTypes =
| BatchDeleteCompletedTasksErrorAction
| DeleteAllCompletedTasksBeginAction
| DeleteAllCompletedTasksSuccessAction
| DeleteAllCompletedTasksErrorAction;
| DeleteAllCompletedTasksErrorAction
| BatchDeleteAggregatingTasksBeginAction
| BatchDeleteAggregatingTasksSuccessAction
| BatchDeleteAggregatingTasksErrorAction
| BatchRunAggregatingTasksBeginAction
| BatchRunAggregatingTasksSuccessAction
| BatchRunAggregatingTasksErrorAction
| RunAllAggregatingTasksBeginAction
| RunAllAggregatingTasksSuccessAction
| RunAllAggregatingTasksErrorAction
| BatchArchiveAggregatingTasksBeginAction
| BatchArchiveAggregatingTasksSuccessAction
| BatchArchiveAggregatingTasksErrorAction
| ArchiveAllAggregatingTasksBeginAction
| ArchiveAllAggregatingTasksSuccessAction
| ArchiveAllAggregatingTasksErrorAction
| DeleteAllAggregatingTasksBeginAction
| DeleteAllAggregatingTasksSuccessAction
| DeleteAllAggregatingTasksErrorAction
| DeleteAggregatingTaskBeginAction
| DeleteAggregatingTaskSuccessAction
| DeleteAggregatingTaskErrorAction
| RunAggregatingTaskBeginAction
| RunAggregatingTaskSuccessAction
| RunAggregatingTaskErrorAction
| ArchiveAggregatingTaskBeginAction
| ArchiveAggregatingTaskSuccessAction
| ArchiveAggregatingTaskErrorAction;
export function getTaskInfoAsync(qname: string, id: string) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
@ -1314,6 +1608,40 @@ export function listCompletedTasksAsync(
};
}
export function listAggregatingTasksAsync(
qname: string,
gname: string,
pageOpts?: PaginationOptions
) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
try {
dispatch({
type: LIST_AGGREGATING_TASKS_BEGIN,
queue: qname,
group: gname,
});
const response = await listAggregatingTasks(qname, gname, pageOpts);
dispatch({
type: LIST_AGGREGATING_TASKS_SUCCESS,
queue: qname,
group: gname,
payload: response,
});
} catch (error) {
console.error(
"listAggregatingTasksAsync: ",
toErrorStringWithHttpStatus(error)
);
dispatch({
type: LIST_AGGREGATING_TASKS_ERROR,
queue: qname,
group: gname,
error: toErrorString(error),
});
}
};
}
export function cancelActiveTaskAsync(queue: string, taskId: string) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: CANCEL_ACTIVE_TASK_BEGIN, queue, taskId });
@ -1679,6 +2007,121 @@ export function batchArchivePendingTasksAsync(
};
}
export function deleteAggregatingTaskAsync(
queue: string,
group: string,
taskId: string
) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: DELETE_AGGREGATING_TASK_BEGIN, queue, taskId });
try {
await deleteAggregatingTask(queue, group, taskId);
dispatch({ type: DELETE_AGGREGATING_TASK_SUCCESS, queue, taskId });
} catch (error) {
console.error(
"deleteAggregatingTaskAsync: ",
toErrorStringWithHttpStatus(error)
);
dispatch({
type: DELETE_AGGREGATING_TASK_ERROR,
error: toErrorString(error),
queue,
taskId,
});
}
};
}
export function runAggregatingTaskAsync(
queue: string,
group: string,
taskId: string
) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: RUN_AGGREGATING_TASK_BEGIN, queue, taskId });
try {
await runAggregatingTask(queue, group, taskId);
dispatch({ type: RUN_AGGREGATING_TASK_SUCCESS, queue, taskId });
} catch (error) {
console.error(
"runAggregatingTaskAsync: ",
toErrorStringWithHttpStatus(error)
);
dispatch({
type: RUN_AGGREGATING_TASK_ERROR,
error: toErrorString(error),
queue,
taskId,
});
}
};
}
export function archiveAggregatingTaskAsync(
queue: string,
group: string,
taskId: string
) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: ARCHIVE_AGGREGATING_TASK_BEGIN, queue, taskId });
try {
await archiveAggregatingTask(queue, group, taskId);
dispatch({ type: ARCHIVE_AGGREGATING_TASK_SUCCESS, queue, taskId });
} catch (error) {
console.error(
"archiveAggregatingTaskAsync: ",
toErrorStringWithHttpStatus(error)
);
dispatch({
type: ARCHIVE_AGGREGATING_TASK_ERROR,
error: toErrorString(error),
queue,
taskId,
});
}
};
}
export function batchArchiveAggregatingTasksAsync(
queue: string,
group: string,
taskIds: string[]
) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({
type: BATCH_ARCHIVE_AGGREGATING_TASKS_BEGIN,
queue,
group,
taskIds,
});
try {
const response = await batchArchiveAggregatingTasks(
queue,
group,
taskIds
);
dispatch({
type: BATCH_ARCHIVE_AGGREGATING_TASKS_SUCCESS,
payload: response,
queue,
group,
});
} catch (error) {
console.error(
"batchArchiveAggregatingTasksAsync: ",
toErrorStringWithHttpStatus(error)
);
dispatch({
type: BATCH_ARCHIVE_AGGREGATING_TASKS_ERROR,
error: toErrorString(error),
queue,
group,
taskIds,
});
}
};
}
export function archiveAllPendingTasksAsync(queue: string) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: ARCHIVE_ALL_PENDING_TASKS_BEGIN, queue });
@ -1723,6 +2166,32 @@ export function deleteAllPendingTasksAsync(queue: string) {
};
}
export function deleteAllAggregatingTasksAsync(queue: string, group: string) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: DELETE_ALL_AGGREGATING_TASKS_BEGIN, queue, group });
try {
const response = await deleteAllAggregatingTasks(queue, group);
dispatch({
type: DELETE_ALL_AGGREGATING_TASKS_SUCCESS,
deleted: response.deleted,
queue,
group,
});
} catch (error) {
console.error(
"deleteAllAggregatingTasksAsync: ",
toErrorStringWithHttpStatus(error)
);
dispatch({
type: DELETE_ALL_AGGREGATING_TASKS_ERROR,
error: toErrorString(error),
queue,
group,
});
}
};
}
export function deleteAllScheduledTasksAsync(queue: string) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: DELETE_ALL_SCHEDULED_TASKS_BEGIN, queue });
@ -1787,6 +2256,58 @@ export function archiveAllScheduledTasksAsync(queue: string) {
};
}
export function archiveAllAggregatingTasksAsync(queue: string, group: string) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: ARCHIVE_ALL_AGGREGATING_TASKS_BEGIN, queue, group });
try {
const response = await archiveAllAggregatingTasks(queue, group);
dispatch({
type: ARCHIVE_ALL_AGGREGATING_TASKS_SUCCESS,
archived: response.archived,
queue,
group,
});
} catch (error) {
console.error(
"archiveAllAggregatingTasksAsync: ",
toErrorStringWithHttpStatus(error)
);
dispatch({
type: ARCHIVE_ALL_AGGREGATING_TASKS_ERROR,
error: toErrorString(error),
queue,
group,
});
}
};
}
export function runAllAggregatingTasksAsync(queue: string, group: string) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: RUN_ALL_AGGREGATING_TASKS_BEGIN, queue, group });
try {
const resp = await runAllAggregatingTasks(queue, group);
dispatch({
type: RUN_ALL_AGGREGATING_TASKS_SUCCESS,
scheduled: resp.scheduled,
queue,
group,
});
} catch (error) {
console.error(
"runAllAggregatingTasksAsync: ",
toErrorStringWithHttpStatus(error)
);
dispatch({
type: RUN_ALL_AGGREGATING_TASKS_ERROR,
error: toErrorString(error),
queue,
group,
});
}
};
}
export function deleteRetryTaskAsync(queue: string, taskId: string) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: DELETE_RETRY_TASK_BEGIN, queue, taskId });
@ -1858,6 +2379,42 @@ export function batchRunRetryTasksAsync(queue: string, taskIds: string[]) {
};
}
export function batchRunAggregatingTasksAsync(
queue: string,
group: string,
taskIds: string[]
) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({
type: BATCH_RUN_AGGREGATING_TASKS_BEGIN,
queue,
group,
taskIds,
});
try {
const response = await batchRunAggregatingTasks(queue, group, taskIds);
dispatch({
type: BATCH_RUN_AGGREGATING_TASKS_SUCCESS,
payload: response,
queue,
group,
});
} catch (error) {
console.error(
"batchRunAggregatingTasksAsync: ",
toErrorStringWithHttpStatus(error)
);
dispatch({
type: BATCH_RUN_AGGREGATING_TASKS_ERROR,
error: toErrorString(error),
queue,
group,
taskIds,
});
}
};
}
export function batchArchiveRetryTasksAsync(queue: string, taskIds: string[]) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: BATCH_ARCHIVE_RETRY_TASKS_BEGIN, queue, taskIds });
@ -2114,6 +2671,42 @@ export function batchDeleteCompletedTasksAsync(
};
}
export function batchDeleteAggregatingTasksAsync(
queue: string,
group: string,
taskIds: string[]
) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({
type: BATCH_DELETE_AGGREGATING_TASKS_BEGIN,
queue,
group,
taskIds,
});
try {
const response = await batchDeleteAggregatingTasks(queue, group, taskIds);
dispatch({
type: BATCH_DELETE_AGGREGATING_TASKS_SUCCESS,
payload: response,
queue,
group,
});
} catch (error) {
console.error(
"batchDeleteAggregatingTasksAsync: ",
toErrorStringWithHttpStatus(error)
);
dispatch({
type: BATCH_DELETE_AGGREGATING_TASKS_ERROR,
error: toErrorString(error),
queue,
group,
taskIds,
});
}
};
}
export function deleteAllCompletedTasksAsync(queue: string) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: DELETE_ALL_COMPLETED_TASKS_BEGIN, queue });

View File

@ -4,7 +4,7 @@ import queryString from "query-string";
// In production build, API server is on listening on the same port as
// the static file server.
// In developement, we assume that the API server is listening on port 8080.
const BASE_URL =
const getBaseUrl = () =>
process.env.NODE_ENV === "production"
? `${window.ROOT_PATH}/api`
: `http://localhost:8080${window.ROOT_PATH}/api`;
@ -18,6 +18,12 @@ export interface ListTasksResponse {
stats: Queue;
}
export interface ListAggregatingTasksResponse {
tasks: TaskInfo[];
stats: Queue;
groups: GroupInfo[];
}
export interface ListServersResponse {
servers: ServerInfo[];
}
@ -54,10 +60,23 @@ export interface DeleteAllTasksResponse {
deleted: number;
}
export interface ArchiveAllTasksResponse {
archived: number;
}
export interface RunAllTasksResponse {
scheduled: number;
}
export interface ListQueueStatsResponse {
stats: { [qname: string]: DailyStat[] };
}
export interface ListGroupsResponse {
stats: Queue;
groups: GroupInfo[];
}
export interface RedisInfoResponse {
address: string;
info: RedisInfo;
@ -250,13 +269,22 @@ export interface RedisInfo {
used_memory_startup: string;
}
export interface GroupInfo {
group: string;
size: number;
}
export interface Queue {
queue: string;
paused: boolean;
size: number;
groups: number;
latency_msec: number;
display_latency: string;
memory_usage_bytes: number;
active: number;
pending: number;
aggregating: number;
scheduled: number;
retry: number;
archived: number;
@ -287,9 +315,11 @@ export interface TaskInfo {
next_process_at: string;
timeout_seconds: number;
deadline: string;
group: string;
completed_at: string;
result: string;
ttl_seconds: number;
is_orphaned: boolean; // Only applies to task.state == 'active'
}
export interface ServerInfo {
@ -337,7 +367,7 @@ export interface PaginationOptions extends Record<string, number | undefined> {
export async function listQueues(): Promise<ListQueuesResponse> {
const resp = await axios({
method: "get",
url: `${BASE_URL}/queues`,
url: `${getBaseUrl()}/queues`,
});
return resp.data;
}
@ -345,28 +375,36 @@ export async function listQueues(): Promise<ListQueuesResponse> {
export async function deleteQueue(qname: string): Promise<void> {
await axios({
method: "delete",
url: `${BASE_URL}/queues/${qname}`,
url: `${getBaseUrl()}/queues/${qname}`,
});
}
export async function pauseQueue(qname: string): Promise<void> {
await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}:pause`,
url: `${getBaseUrl()}/queues/${qname}:pause`,
});
}
export async function resumeQueue(qname: string): Promise<void> {
await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}:resume`,
url: `${getBaseUrl()}/queues/${qname}:resume`,
});
}
export async function listQueueStats(): Promise<ListQueueStatsResponse> {
const resp = await axios({
method: "get",
url: `${BASE_URL}/queue_stats`,
url: `${getBaseUrl()}/queue_stats`,
});
return resp.data;
}
export async function listGroups(qname: string): Promise<ListGroupsResponse> {
const resp = await axios({
method: "get",
url: `${getBaseUrl()}/queues/${qname}/groups`,
});
return resp.data;
}
@ -375,7 +413,7 @@ export async function getTaskInfo(
qname: string,
id: string
): Promise<TaskInfo> {
const url = `${BASE_URL}/queues/${qname}/tasks/${id}`;
const url = `${getBaseUrl()}/queues/${qname}/tasks/${id}`;
const resp = await axios({
method: "get",
url,
@ -387,7 +425,7 @@ export async function listActiveTasks(
qname: string,
pageOpts?: PaginationOptions
): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/active_tasks`;
let url = `${getBaseUrl()}/queues/${qname}/active_tasks`;
if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`;
}
@ -404,14 +442,14 @@ export async function cancelActiveTask(
): Promise<void> {
await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/active_tasks/${taskId}:cancel`,
url: `${getBaseUrl()}/queues/${qname}/active_tasks/${taskId}:cancel`,
});
}
export async function cancelAllActiveTasks(qname: string): Promise<void> {
await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/active_tasks:cancel_all`,
url: `${getBaseUrl()}/queues/${qname}/active_tasks:cancel_all`,
});
}
@ -421,7 +459,7 @@ export async function batchCancelActiveTasks(
): Promise<BatchCancelTasksResponse> {
const resp = await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/active_tasks:batch_cancel`,
url: `${getBaseUrl()}/queues/${qname}/active_tasks:batch_cancel`,
data: {
task_ids: taskIds,
},
@ -433,7 +471,7 @@ export async function listPendingTasks(
qname: string,
pageOpts?: PaginationOptions
): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/pending_tasks`;
let url = `${getBaseUrl()}/queues/${qname}/pending_tasks`;
if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`;
}
@ -448,7 +486,7 @@ export async function listScheduledTasks(
qname: string,
pageOpts?: PaginationOptions
): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/scheduled_tasks`;
let url = `${getBaseUrl()}/queues/${qname}/scheduled_tasks`;
if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`;
}
@ -463,7 +501,7 @@ export async function listRetryTasks(
qname: string,
pageOpts?: PaginationOptions
): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/retry_tasks`;
let url = `${getBaseUrl()}/queues/${qname}/retry_tasks`;
if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`;
}
@ -478,7 +516,7 @@ export async function listArchivedTasks(
qname: string,
pageOpts?: PaginationOptions
): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/archived_tasks`;
let url = `${getBaseUrl()}/queues/${qname}/archived_tasks`;
if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`;
}
@ -493,7 +531,23 @@ export async function listCompletedTasks(
qname: string,
pageOpts?: PaginationOptions
): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/completed_tasks`;
let url = `${getBaseUrl()}/queues/${qname}/completed_tasks`;
if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`;
}
const resp = await axios({
method: "get",
url,
});
return resp.data;
}
export async function listAggregatingTasks(
qname: string,
gname: string,
pageOpts?: PaginationOptions
): Promise<ListAggregatingTasksResponse> {
let url = `${getBaseUrl()}/queues/${qname}/groups/${gname}/aggregating_tasks`;
if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`;
}
@ -510,7 +564,7 @@ export async function archivePendingTask(
): Promise<void> {
await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/pending_tasks/${taskId}:archive`,
url: `${getBaseUrl()}/queues/${qname}/pending_tasks/${taskId}:archive`,
});
}
@ -520,7 +574,7 @@ export async function batchArchivePendingTasks(
): Promise<BatchArchiveTasksResponse> {
const resp = await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/pending_tasks:batch_archive`,
url: `${getBaseUrl()}/queues/${qname}/pending_tasks:batch_archive`,
data: {
task_ids: taskIds,
},
@ -531,7 +585,7 @@ export async function batchArchivePendingTasks(
export async function archiveAllPendingTasks(qname: string): Promise<void> {
await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/pending_tasks:archive_all`,
url: `${getBaseUrl()}/queues/${qname}/pending_tasks:archive_all`,
});
}
@ -541,7 +595,7 @@ export async function deletePendingTask(
): Promise<void> {
await axios({
method: "delete",
url: `${BASE_URL}/queues/${qname}/pending_tasks/${taskId}`,
url: `${getBaseUrl()}/queues/${qname}/pending_tasks/${taskId}`,
});
}
@ -551,7 +605,7 @@ export async function batchDeletePendingTasks(
): Promise<BatchDeleteTasksResponse> {
const resp = await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/pending_tasks:batch_delete`,
url: `${getBaseUrl()}/queues/${qname}/pending_tasks:batch_delete`,
data: {
task_ids: taskIds,
},
@ -564,7 +618,118 @@ export async function deleteAllPendingTasks(
): Promise<DeleteAllTasksResponse> {
const resp = await axios({
method: "delete",
url: `${BASE_URL}/queues/${qname}/pending_tasks:delete_all`,
url: `${getBaseUrl()}/queues/${qname}/pending_tasks:delete_all`,
});
return resp.data;
}
export async function deleteAggregatingTask(
qname: string,
gname: string,
taskId: string
): Promise<void> {
await axios({
method: "delete",
url: `${getBaseUrl()}/queues/${qname}/groups/${gname}/aggregating_tasks/${taskId}`,
});
}
export async function batchDeleteAggregatingTasks(
qname: string,
gname: string,
taskIds: string[]
): Promise<BatchDeleteTasksResponse> {
const resp = await axios({
method: "post",
url: `${getBaseUrl()}/queues/${qname}/groups/${gname}/aggregating_tasks:batch_delete`,
data: {
task_ids: taskIds,
},
});
return resp.data;
}
export async function deleteAllAggregatingTasks(
qname: string,
gname: string
): Promise<DeleteAllTasksResponse> {
const resp = await axios({
method: "delete",
url: `${getBaseUrl()}/queues/${qname}/groups/${gname}/aggregating_tasks:delete_all`,
});
return resp.data;
}
export async function runAggregatingTask(
qname: string,
gname: string,
taskId: string
): Promise<void> {
await axios({
method: "post",
url: `${getBaseUrl()}/queues/${qname}/groups/${gname}/aggregating_tasks/${taskId}:run`,
});
}
export async function batchRunAggregatingTasks(
qname: string,
gname: string,
taskIds: string[]
): Promise<BatchRunTasksResponse> {
const resp = await axios({
method: "post",
url: `${getBaseUrl()}/queues/${qname}/groups/${gname}/aggregating_tasks:batch_run`,
data: {
task_ids: taskIds,
},
});
return resp.data;
}
export async function runAllAggregatingTasks(
qname: string,
gname: string
): Promise<RunAllTasksResponse> {
const resp = await axios({
method: "post",
url: `${getBaseUrl()}/queues/${qname}/groups/${gname}/aggregating_tasks:run_all`,
});
return resp.data;
}
export async function archiveAggregatingTask(
qname: string,
gname: string,
taskId: string
): Promise<void> {
await axios({
method: "post",
url: `${getBaseUrl()}/queues/${qname}/groups/${gname}/aggregating_tasks/${taskId}:archive`,
});
}
export async function batchArchiveAggregatingTasks(
qname: string,
gname: string,
taskIds: string[]
): Promise<BatchArchiveTasksResponse> {
const resp = await axios({
method: "post",
url: `${getBaseUrl()}/queues/${qname}/groups/${gname}/aggregating_tasks:batch_archive`,
data: {
task_ids: taskIds,
},
});
return resp.data;
}
export async function archiveAllAggregatingTasks(
qname: string,
gname: string
): Promise<ArchiveAllTasksResponse> {
const resp = await axios({
method: "post",
url: `${getBaseUrl()}/queues/${qname}/groups/${gname}/aggregating_tasks:archive_all`,
});
return resp.data;
}
@ -575,7 +740,7 @@ export async function runScheduledTask(
): Promise<void> {
await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/scheduled_tasks/${taskId}:run`,
url: `${getBaseUrl()}/queues/${qname}/scheduled_tasks/${taskId}:run`,
});
}
@ -585,7 +750,7 @@ export async function archiveScheduledTask(
): Promise<void> {
await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/scheduled_tasks/${taskId}:archive`,
url: `${getBaseUrl()}/queues/${qname}/scheduled_tasks/${taskId}:archive`,
});
}
@ -595,7 +760,7 @@ export async function deleteScheduledTask(
): Promise<void> {
await axios({
method: "delete",
url: `${BASE_URL}/queues/${qname}/scheduled_tasks/${taskId}`,
url: `${getBaseUrl()}/queues/${qname}/scheduled_tasks/${taskId}`,
});
}
@ -605,7 +770,7 @@ export async function batchDeleteScheduledTasks(
): Promise<BatchDeleteTasksResponse> {
const resp = await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/scheduled_tasks:batch_delete`,
url: `${getBaseUrl()}/queues/${qname}/scheduled_tasks:batch_delete`,
data: {
task_ids: taskIds,
},
@ -618,7 +783,7 @@ export async function deleteAllScheduledTasks(
): Promise<DeleteAllTasksResponse> {
const resp = await axios({
method: "delete",
url: `${BASE_URL}/queues/${qname}/scheduled_tasks:delete_all`,
url: `${getBaseUrl()}/queues/${qname}/scheduled_tasks:delete_all`,
});
return resp.data;
}
@ -629,7 +794,7 @@ export async function batchRunScheduledTasks(
): Promise<BatchRunTasksResponse> {
const resp = await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/scheduled_tasks:batch_run`,
url: `${getBaseUrl()}/queues/${qname}/scheduled_tasks:batch_run`,
data: {
task_ids: taskIds,
},
@ -640,7 +805,7 @@ export async function batchRunScheduledTasks(
export async function runAllScheduledTasks(qname: string): Promise<void> {
await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/scheduled_tasks:run_all`,
url: `${getBaseUrl()}/queues/${qname}/scheduled_tasks:run_all`,
});
}
@ -650,7 +815,7 @@ export async function batchArchiveScheduledTasks(
): Promise<BatchArchiveTasksResponse> {
const resp = await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/scheduled_tasks:batch_archive`,
url: `${getBaseUrl()}/queues/${qname}/scheduled_tasks:batch_archive`,
data: {
task_ids: taskIds,
},
@ -661,7 +826,7 @@ export async function batchArchiveScheduledTasks(
export async function archiveAllScheduledTasks(qname: string): Promise<void> {
await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/scheduled_tasks:archive_all`,
url: `${getBaseUrl()}/queues/${qname}/scheduled_tasks:archive_all`,
});
}
@ -671,7 +836,7 @@ export async function runRetryTask(
): Promise<void> {
await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/retry_tasks/${taskId}:run`,
url: `${getBaseUrl()}/queues/${qname}/retry_tasks/${taskId}:run`,
});
}
@ -681,7 +846,7 @@ export async function archiveRetryTask(
): Promise<void> {
await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/retry_tasks/${taskId}:archive`,
url: `${getBaseUrl()}/queues/${qname}/retry_tasks/${taskId}:archive`,
});
}
@ -691,7 +856,7 @@ export async function deleteRetryTask(
): Promise<void> {
await axios({
method: "delete",
url: `${BASE_URL}/queues/${qname}/retry_tasks/${taskId}`,
url: `${getBaseUrl()}/queues/${qname}/retry_tasks/${taskId}`,
});
}
@ -701,7 +866,7 @@ export async function batchDeleteRetryTasks(
): Promise<BatchDeleteTasksResponse> {
const resp = await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/retry_tasks:batch_delete`,
url: `${getBaseUrl()}/queues/${qname}/retry_tasks:batch_delete`,
data: {
task_ids: taskIds,
},
@ -714,7 +879,7 @@ export async function deleteAllRetryTasks(
): Promise<DeleteAllTasksResponse> {
const resp = await axios({
method: "delete",
url: `${BASE_URL}/queues/${qname}/retry_tasks:delete_all`,
url: `${getBaseUrl()}/queues/${qname}/retry_tasks:delete_all`,
});
return resp.data;
}
@ -725,7 +890,7 @@ export async function batchRunRetryTasks(
): Promise<BatchRunTasksResponse> {
const resp = await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/retry_tasks:batch_run`,
url: `${getBaseUrl()}/queues/${qname}/retry_tasks:batch_run`,
data: {
task_ids: taskIds,
},
@ -736,7 +901,7 @@ export async function batchRunRetryTasks(
export async function runAllRetryTasks(qname: string): Promise<void> {
await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/retry_tasks:run_all`,
url: `${getBaseUrl()}/queues/${qname}/retry_tasks:run_all`,
});
}
@ -746,7 +911,7 @@ export async function batchArchiveRetryTasks(
): Promise<BatchArchiveTasksResponse> {
const resp = await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/retry_tasks:batch_archive`,
url: `${getBaseUrl()}/queues/${qname}/retry_tasks:batch_archive`,
data: {
task_ids: taskIds,
},
@ -757,7 +922,7 @@ export async function batchArchiveRetryTasks(
export async function archiveAllRetryTasks(qname: string): Promise<void> {
await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/retry_tasks:archive_all`,
url: `${getBaseUrl()}/queues/${qname}/retry_tasks:archive_all`,
});
}
@ -767,7 +932,7 @@ export async function runArchivedTask(
): Promise<void> {
await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/archived_tasks/${taskId}:run`,
url: `${getBaseUrl()}/queues/${qname}/archived_tasks/${taskId}:run`,
});
}
@ -777,7 +942,7 @@ export async function deleteArchivedTask(
): Promise<void> {
await axios({
method: "delete",
url: `${BASE_URL}/queues/${qname}/archived_tasks/${taskId}`,
url: `${getBaseUrl()}/queues/${qname}/archived_tasks/${taskId}`,
});
}
@ -787,7 +952,7 @@ export async function batchDeleteArchivedTasks(
): Promise<BatchDeleteTasksResponse> {
const resp = await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/archived_tasks:batch_delete`,
url: `${getBaseUrl()}/queues/${qname}/archived_tasks:batch_delete`,
data: {
task_ids: taskIds,
},
@ -800,7 +965,7 @@ export async function deleteAllArchivedTasks(
): Promise<DeleteAllTasksResponse> {
const resp = await axios({
method: "delete",
url: `${BASE_URL}/queues/${qname}/archived_tasks:delete_all`,
url: `${getBaseUrl()}/queues/${qname}/archived_tasks:delete_all`,
});
return resp.data;
}
@ -811,7 +976,7 @@ export async function batchRunArchivedTasks(
): Promise<BatchRunTasksResponse> {
const resp = await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/archived_tasks:batch_run`,
url: `${getBaseUrl()}/queues/${qname}/archived_tasks:batch_run`,
data: {
task_ids: taskIds,
},
@ -822,7 +987,7 @@ export async function batchRunArchivedTasks(
export async function runAllArchivedTasks(qname: string): Promise<void> {
await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/archived_tasks:run_all`,
url: `${getBaseUrl()}/queues/${qname}/archived_tasks:run_all`,
});
}
@ -832,7 +997,7 @@ export async function deleteCompletedTask(
): Promise<void> {
await axios({
method: "delete",
url: `${BASE_URL}/queues/${qname}/completed_tasks/${taskId}`,
url: `${getBaseUrl()}/queues/${qname}/completed_tasks/${taskId}`,
});
}
@ -842,7 +1007,7 @@ export async function batchDeleteCompletedTasks(
): Promise<BatchDeleteTasksResponse> {
const resp = await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/completed_tasks:batch_delete`,
url: `${getBaseUrl()}/queues/${qname}/completed_tasks:batch_delete`,
data: {
task_ids: taskIds,
},
@ -855,7 +1020,7 @@ export async function deleteAllCompletedTasks(
): Promise<DeleteAllTasksResponse> {
const resp = await axios({
method: "delete",
url: `${BASE_URL}/queues/${qname}/completed_tasks:delete_all`,
url: `${getBaseUrl()}/queues/${qname}/completed_tasks:delete_all`,
});
return resp.data;
}
@ -863,7 +1028,7 @@ export async function deleteAllCompletedTasks(
export async function listServers(): Promise<ListServersResponse> {
const resp = await axios({
method: "get",
url: `${BASE_URL}/servers`,
url: `${getBaseUrl()}/servers`,
});
return resp.data;
}
@ -871,7 +1036,7 @@ export async function listServers(): Promise<ListServersResponse> {
export async function listSchedulerEntries(): Promise<ListSchedulerEntriesResponse> {
const resp = await axios({
method: "get",
url: `${BASE_URL}/scheduler_entries`,
url: `${getBaseUrl()}/scheduler_entries`,
});
return resp.data;
}
@ -881,7 +1046,7 @@ export async function listSchedulerEnqueueEvents(
): Promise<ListSchedulerEnqueueEventsResponse> {
const resp = await axios({
method: "get",
url: `${BASE_URL}/scheduler_entries/${entryId}/enqueue_events`,
url: `${getBaseUrl()}/scheduler_entries/${entryId}/enqueue_events`,
});
return resp.data;
}
@ -889,7 +1054,7 @@ export async function listSchedulerEnqueueEvents(
export async function getRedisInfo(): Promise<RedisInfoResponse> {
const resp = await axios({
method: "get",
url: `${BASE_URL}/redis_info`,
url: `${getBaseUrl()}/redis_info`,
});
return resp.data;
}
@ -914,7 +1079,7 @@ export async function getMetrics(
}
const resp = await axios({
method: "get",
url: `${BASE_URL}/metrics?${queryString.stringify(params)}`,
url: `${getBaseUrl()}/metrics?${queryString.stringify(params)}`,
});
return resp.data;
}

View File

@ -1,57 +1,27 @@
import React, { useState, useCallback } 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 Alert from "@material-ui/lab/Alert";
import AlertTitle from "@material-ui/lab/AlertTitle";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableFooter from "@material-ui/core/TableFooter";
import TablePagination from "@material-ui/core/TablePagination";
import Tooltip from "@material-ui/core/Tooltip";
import Paper from "@material-ui/core/Paper";
import Checkbox from "@material-ui/core/Checkbox";
import IconButton from "@material-ui/core/IconButton";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import Tooltip from "@material-ui/core/Tooltip";
import CancelIcon from "@material-ui/icons/Cancel";
import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined";
import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
import SyntaxHighlighter from "./SyntaxHighlighter";
import {
listActiveTasksAsync,
cancelActiveTaskAsync,
batchCancelActiveTasksAsync,
cancelAllActiveTasksAsync,
} from "../actions/tasksActions";
import React from "react";
import { connect, ConnectedProps } from "react-redux";
import { useHistory } from "react-router-dom";
import { taskRowsPerPageChange } from "../actions/settingsActions";
import { AppState } from "../store";
import TablePaginationActions, {
rowsPerPageOptions,
} from "./TablePaginationActions";
import TableActions from "./TableActions";
import { usePolling } from "../hooks";
import { ActiveTaskExtended } from "../reducers/tasksReducer";
import { durationBefore, timeAgo, uuidPrefix, prettifyPayload } from "../utils";
import { TableColumn } from "../types/table";
import {
batchCancelActiveTasksAsync,
cancelActiveTaskAsync,
cancelAllActiveTasksAsync,
listActiveTasksAsync,
} from "../actions/tasksActions";
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",
},
}));
import { AppState } from "../store";
import { TableColumn } from "../types/table";
import { durationBefore, prettifyPayload, timeAgo, uuidPrefix } from "../utils";
import SyntaxHighlighter from "./SyntaxHighlighter";
import TasksTable, { RowProps, useRowStyles } from "./TasksTable";
function mapStateToProps(state: AppState) {
return {
@ -66,10 +36,10 @@ function mapStateToProps(state: AppState) {
}
const mapDispatchToProps = {
listActiveTasksAsync,
cancelActiveTaskAsync,
batchCancelActiveTasksAsync,
cancelAllActiveTasksAsync,
listTasks: listActiveTasksAsync,
cancelTask: cancelActiveTaskAsync,
batchCancelTasks: batchCancelActiveTasksAsync,
cancelAllTasks: cancelAllActiveTasksAsync,
taskRowsPerPageChange,
};
@ -89,198 +59,7 @@ type ReduxProps = ConnectedProps<typeof connector>;
interface Props {
queue: string; // name of the queue
}
function ActiveTasksTable(props: Props & ReduxProps) {
const { pollInterval, listActiveTasksAsync, 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 handleCancelAllClick = () => {
props.cancelAllActiveTasksAsync(queue);
};
const handleBatchCancelClick = () => {
props
.batchCancelActiveTasksAsync(queue, selectedIds)
.then(() => setSelectedIds([]));
};
const fetchData = useCallback(() => {
const pageOpts = { page: page + 1, size: pageSize };
listActiveTasksAsync(queue, pageOpts);
}, [page, pageSize, queue, listActiveTasksAsync]);
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 active tasks at this time.
</Alert>
);
}
const rowCount = props.tasks.length;
const numSelected = selectedIds.length;
return (
<div>
<TableActions
showIconButtons={numSelected > 0}
iconButtonActions={[
{
tooltip: "Cancel",
icon: <CancelIcon />,
onClick: handleBatchCancelClick,
disabled: props.batchActionPending,
},
]}
menuItemActions={[
{
label: "Cancel All",
onClick: handleCancelAllClick,
disabled: props.allActionPending,
},
]}
/>
<TableContainer component={Paper}>
<Table
stickyHeader={true}
className={classes.table}
aria-label="active 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>
{/* TODO: loading and empty state */}
{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));
}
}}
onCancelClick={() => {
props.cancelActiveTaskAsync(queue, task.id);
}}
onActionCellEnter={() => setActiveTaskId(task.id)}
onActionCellLeave={() => setActiveTaskId("")}
showActions={activeTaskId === task.id}
/>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={rowsPerPageOptions}
colSpan={columns.length + 1}
count={props.tasks.length}
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,
},
},
}));
interface RowProps {
task: ActiveTaskExtended;
isSelected: boolean;
onSelectChange: (checked: boolean) => void;
onCancelClick: () => void;
showActions: boolean;
onActionCellEnter: () => void;
onActionCellLeave: () => void;
totalTaskCount: number; // total number of active tasks
}
function Row(props: RowProps) {
@ -294,18 +73,34 @@ function Row(props: RowProps) {
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)}
{!window.READ_ONLY && (
<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" className={classes.idCell}>
<div className={classes.IdGroup}>
{uuidPrefix(task.id)}
<Tooltip title="Copy full ID to clipboard">
<IconButton
onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(task.id);
}}
size="small"
className={classes.copyButton}
>
<FileCopyOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
</div>
</TableCell>
<TableCell>{task.type}</TableCell>
<TableCell>
@ -316,39 +111,64 @@ function Row(props: RowProps) {
{prettifyPayload(task.payload)}
</SyntaxHighlighter>
</TableCell>
<TableCell>{task.canceling ? "Canceling" : "Running"}</TableCell>
<TableCell>
{task.start_time === "-" ? "just now" : timeAgo(task.start_time)}
{task.canceling
? "Canceling"
: task.is_orphaned
? "Orphaned"
: "Running"}
</TableCell>
<TableCell>
{task.is_orphaned
? "-"
: task.start_time === "-"
? "just now"
: timeAgo(task.start_time)}
</TableCell>
<TableCell>
{task.deadline === "-" ? "-" : durationBefore(task.deadline)}
</TableCell>
<TableCell
align="center"
onMouseEnter={props.onActionCellEnter}
onMouseLeave={props.onActionCellLeave}
onClick={(e) => e.stopPropagation()}
>
{props.showActions ? (
<React.Fragment>
<Tooltip title="Cancel">
<IconButton
onClick={props.onCancelClick}
disabled={task.requestPending || task.canceling}
size="small"
>
<CancelIcon fontSize="small" />
</IconButton>
</Tooltip>
</React.Fragment>
) : (
<IconButton size="small" onClick={props.onActionCellEnter}>
<MoreHorizIcon fontSize="small" />
</IconButton>
)}
</TableCell>
{!window.READ_ONLY && (
<TableCell
align="center"
onMouseEnter={props.onActionCellEnter}
onMouseLeave={props.onActionCellLeave}
onClick={(e) => e.stopPropagation()}
>
{props.showActions ? (
<React.Fragment>
<Tooltip title="Cancel">
<IconButton
onClick={props.onCancelClick}
disabled={
task.requestPending || task.canceling || task.is_orphaned
}
size="small"
>
<CancelIcon fontSize="small" />
</IconButton>
</Tooltip>
</React.Fragment>
) : (
<IconButton size="small" onClick={props.onActionCellEnter}>
<MoreHorizIcon fontSize="small" />
</IconButton>
)}
</TableCell>
)}
</TableRow>
);
}
function ActiveTasksTable(props: Props & ReduxProps) {
return (
<TasksTable
taskState="active"
columns={columns}
renderRow={(rowProps: RowProps) => <Row {...rowProps} />}
{...props}
/>
);
}
export default connector(ActiveTasksTable);

View File

@ -0,0 +1,247 @@
import Checkbox from "@material-ui/core/Checkbox";
import IconButton from "@material-ui/core/IconButton";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import Tooltip from "@material-ui/core/Tooltip";
import ArchiveIcon from "@material-ui/icons/Archive";
import DeleteIcon from "@material-ui/icons/Delete";
import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined";
import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import React from "react";
import { connect, ConnectedProps } from "react-redux";
import { useHistory } from "react-router-dom";
import { taskRowsPerPageChange } from "../actions/settingsActions";
import {
archiveAggregatingTaskAsync,
archiveAllAggregatingTasksAsync,
batchArchiveAggregatingTasksAsync,
batchDeleteAggregatingTasksAsync,
batchRunAggregatingTasksAsync,
deleteAggregatingTaskAsync,
deleteAllAggregatingTasksAsync,
listAggregatingTasksAsync,
runAggregatingTaskAsync,
runAllAggregatingTasksAsync,
} from "../actions/tasksActions";
import { PaginationOptions } from "../api";
import { taskDetailsPath } from "../paths";
import { AppState } from "../store";
import { TableColumn } from "../types/table";
import { prettifyPayload, uuidPrefix } from "../utils";
import SyntaxHighlighter from "./SyntaxHighlighter";
import TasksTable, { RowProps, useRowStyles } from "./TasksTable";
function mapStateToProps(state: AppState) {
return {
groups: state.groups.data,
groupsError: state.groups.error,
loading: state.tasks.aggregatingTasks.loading,
allActionPending: state.tasks.aggregatingTasks.allActionPending,
batchActionPending: state.tasks.aggregatingTasks.batchActionPending,
error: state.tasks.aggregatingTasks.error,
group: state.tasks.aggregatingTasks.group,
tasks: state.tasks.aggregatingTasks.data,
pollInterval: state.settings.pollInterval,
pageSize: state.settings.taskRowsPerPage,
};
}
const mapDispatchToProps = {
listAggregatingTasksAsync,
deleteAllAggregatingTasksAsync,
archiveAllAggregatingTasksAsync,
runAllAggregatingTasksAsync,
batchDeleteAggregatingTasksAsync,
batchRunAggregatingTasksAsync,
batchArchiveAggregatingTasksAsync,
deleteAggregatingTaskAsync,
runAggregatingTaskAsync,
archiveAggregatingTaskAsync,
taskRowsPerPageChange,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
type ReduxProps = ConnectedProps<typeof connector>;
interface Props {
queue: string;
selectedGroup: string;
totalTaskCount: number; // total number of tasks in the group
}
const columns: TableColumn[] = [
{ key: "id", label: "ID", align: "left" },
{ key: "type", label: "Type", align: "left" },
{ key: "paylod", label: "Payload", align: "left" },
{ key: "group", label: "Group", align: "left" },
{ key: "actions", label: "Actions", align: "center" },
];
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))}
>
{!window.READ_ONLY && (
<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" className={classes.idCell}>
<div className={classes.IdGroup}>
{uuidPrefix(task.id)}
<Tooltip title="Copy full ID to clipboard">
<IconButton
onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(task.id);
}}
size="small"
className={classes.copyButton}
>
<FileCopyOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
</div>
</TableCell>
<TableCell>{task.type}</TableCell>
<TableCell>
<SyntaxHighlighter
language="json"
customStyle={{ margin: 0, maxWidth: 400 }}
>
{prettifyPayload(task.payload)}
</SyntaxHighlighter>
</TableCell>
<TableCell>{task.group}</TableCell>
{!window.READ_ONLY && (
<TableCell
align="center"
className={classes.actionCell}
onMouseEnter={props.onActionCellEnter}
onMouseLeave={props.onActionCellLeave}
onClick={(e) => e.stopPropagation()}
>
{props.showActions ? (
<React.Fragment>
<Tooltip title="Delete">
<IconButton
onClick={props.onDeleteClick}
disabled={task.requestPending || props.allActionPending}
size="small"
className={classes.actionButton}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Archive">
<IconButton
onClick={props.onArchiveClick}
disabled={task.requestPending || props.allActionPending}
size="small"
className={classes.actionButton}
>
<ArchiveIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Run">
<IconButton
onClick={props.onRunClick}
disabled={task.requestPending || props.allActionPending}
size="small"
className={classes.actionButton}
>
<PlayArrowIcon fontSize="small" />
</IconButton>
</Tooltip>
</React.Fragment>
) : (
<IconButton size="small" onClick={props.onActionCellEnter}>
<MoreHorizIcon fontSize="small" />
</IconButton>
)}
</TableCell>
)}
</TableRow>
);
}
function AggregatingTasksTable(props: Props & ReduxProps) {
const listTasks = (qname: string, pgn?: PaginationOptions) =>
props.listAggregatingTasksAsync(qname, props.selectedGroup, pgn);
const deleteAllTasks = (qname: string) =>
props.deleteAllAggregatingTasksAsync(qname, props.selectedGroup);
const archiveAllTasks = (qname: string) =>
props.archiveAllAggregatingTasksAsync(qname, props.selectedGroup);
const runAllTasks = (qname: string) =>
props.runAllAggregatingTasksAsync(qname, props.selectedGroup);
const batchDeleteTasks = (qname: string, taskIds: string[]) =>
props.batchDeleteAggregatingTasksAsync(qname, props.selectedGroup, taskIds);
const batchArchiveTasks = (qname: string, taskIds: string[]) =>
props.batchArchiveAggregatingTasksAsync(
qname,
props.selectedGroup,
taskIds
);
const batchRunTasks = (qname: string, taskIds: string[]) =>
props.batchRunAggregatingTasksAsync(qname, props.selectedGroup, taskIds);
const deleteTask = (qname: string, taskId: string) =>
props.deleteAggregatingTaskAsync(qname, props.selectedGroup, taskId);
const archiveTask = (qname: string, taskId: string) =>
props.archiveAggregatingTaskAsync(qname, props.selectedGroup, taskId);
const runTask = (qname: string, taskId: string) =>
props.runAggregatingTaskAsync(qname, props.selectedGroup, taskId);
return (
<TasksTable
queue={props.queue}
totalTaskCount={props.totalTaskCount}
taskState="aggregating"
loading={props.loading}
error={props.error}
tasks={props.tasks}
batchActionPending={props.batchActionPending}
allActionPending={props.allActionPending}
pollInterval={props.pollInterval}
pageSize={props.pageSize}
listTasks={listTasks}
deleteAllTasks={deleteAllTasks}
archiveAllTasks={archiveAllTasks}
runAllTasks={runAllTasks}
batchDeleteTasks={batchDeleteTasks}
batchArchiveTasks={batchArchiveTasks}
batchRunTasks={batchRunTasks}
deleteTask={deleteTask}
archiveTask={archiveTask}
runTask={runTask}
taskRowsPerPageChange={props.taskRowsPerPageChange}
columns={columns}
renderRow={(rowProps: RowProps) => <Row {...rowProps} />}
/>
);
}
export default connector(AggregatingTasksTable);

View File

@ -0,0 +1,100 @@
import { makeStyles } from "@material-ui/core/styles";
import Alert from "@material-ui/lab/Alert";
import AlertTitle from "@material-ui/lab/AlertTitle";
import React, { useCallback, useState } from "react";
import { connect, ConnectedProps } from "react-redux";
import { listGroupsAsync } from "../actions/groupsActions";
import { GroupInfo } from "../api";
import { usePolling } from "../hooks";
import { AppState } from "../store";
import AggregatingTasksTable from "./AggregatingTasksTable";
import GroupSelect from "./GroupSelect";
const useStyles = makeStyles((theme) => ({
groupSelector: {
paddingTop: theme.spacing(1),
paddingBottom: theme.spacing(1),
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
},
alert: {
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
},
}));
function mapStateToProps(state: AppState) {
return {
groups: state.groups.data,
groupsError: state.groups.error,
pollInterval: state.settings.pollInterval,
};
}
const mapDispatchToProps = {
listGroupsAsync,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
interface Props {
queue: string;
}
function AggregatingTasksTableContainer(
props: Props & ConnectedProps<typeof connector>
) {
const [selectedGroup, setSelectedGroup] = useState<GroupInfo | null>(null);
const { pollInterval, listGroupsAsync, queue } = props;
const classes = useStyles();
const fetchGroups = useCallback(() => {
listGroupsAsync(queue);
}, [listGroupsAsync, queue]);
usePolling(fetchGroups, pollInterval);
if (props.groupsError.length > 0) {
return (
<Alert severity="error" className={classes.alert}>
<AlertTitle>Error</AlertTitle>
{props.groupsError}
</Alert>
);
}
if (props.groups.length === 0) {
return (
<Alert severity="info" className={classes.alert}>
<AlertTitle>Info</AlertTitle>
No aggregating tasks at this time.
</Alert>
);
}
return (
<div>
<div className={classes.groupSelector}>
<GroupSelect
selected={selectedGroup}
onSelect={setSelectedGroup}
groups={props.groups}
error={props.groupsError}
/>
</div>
{selectedGroup !== null ? (
<AggregatingTasksTable
queue={props.queue}
totalTaskCount={selectedGroup.size}
selectedGroup={selectedGroup.group}
/>
) : (
<Alert severity="info" className={classes.alert}>
<AlertTitle>Info</AlertTitle>
<div>Please select group</div>
</Alert>
)}
</div>
);
}
export default connector(AggregatingTasksTableContainer);

View File

@ -1,61 +1,31 @@
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 IconButton from "@material-ui/core/IconButton";
import TableCell from "@material-ui/core/TableCell";
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 PlayArrowIcon from "@material-ui/icons/PlayArrow";
import DeleteIcon from "@material-ui/icons/Delete";
import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined";
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 PlayArrowIcon from "@material-ui/icons/PlayArrow";
import React from "react";
import { connect, ConnectedProps } from "react-redux";
import { useHistory } from "react-router-dom";
import { taskRowsPerPageChange } from "../actions/settingsActions";
import {
batchDeleteArchivedTasksAsync,
batchRunArchivedTasksAsync,
deleteArchivedTaskAsync,
deleteAllArchivedTasksAsync,
deleteArchivedTaskAsync,
listArchivedTasksAsync,
runArchivedTaskAsync,
runAllArchivedTasksAsync,
runArchivedTaskAsync,
} from "../actions/tasksActions";
import TablePaginationActions, {
rowsPerPageOptions,
} from "./TablePaginationActions";
import { taskRowsPerPageChange } from "../actions/settingsActions";
import TableActions from "./TableActions";
import { timeAgo, uuidPrefix, prettifyPayload } 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",
},
}));
import { AppState } from "../store";
import { TableColumn } from "../types/table";
import { prettifyPayload, timeAgo, uuidPrefix } from "../utils";
import SyntaxHighlighter from "./SyntaxHighlighter";
import TasksTable, { RowProps, useRowStyles } from "./TasksTable";
function mapStateToProps(state: AppState) {
return {
@ -70,13 +40,13 @@ function mapStateToProps(state: AppState) {
}
const mapDispatchToProps = {
listArchivedTasksAsync,
runArchivedTaskAsync,
runAllArchivedTasksAsync,
deleteArchivedTaskAsync,
deleteAllArchivedTasksAsync,
batchRunArchivedTasksAsync,
batchDeleteArchivedTasksAsync,
listTasks: listArchivedTasksAsync,
runTask: runArchivedTaskAsync,
runAllTasks: runAllArchivedTasksAsync,
deleteTask: deleteArchivedTaskAsync,
deleteAllTasks: deleteAllArchivedTasksAsync,
batchRunTasks: batchRunArchivedTasksAsync,
batchDeleteTasks: batchDeleteArchivedTasksAsync,
taskRowsPerPageChange,
};
@ -89,238 +59,14 @@ interface Props {
totalTaskCount: number; // totoal number of archived tasks.
}
function ArchivedTasksTable(props: Props & ReduxProps) {
const { pollInterval, listArchivedTasksAsync, 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 handleRunAllClick = () => {
props.runAllArchivedTasksAsync(queue);
};
const handleDeleteAllClick = () => {
props.deleteAllArchivedTasksAsync(queue);
};
const handleBatchRunClick = () => {
props
.batchRunArchivedTasksAsync(queue, selectedIds)
.then(() => setSelectedIds([]));
};
const handleBatchDeleteClick = () => {
props
.batchDeleteArchivedTasksAsync(queue, selectedIds)
.then(() => setSelectedIds([]));
};
const fetchData = useCallback(() => {
const pageOpts = { page: page + 1, size: pageSize };
listArchivedTasksAsync(queue, pageOpts);
}, [page, pageSize, queue, listArchivedTasksAsync]);
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 archived 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: "last_failed", label: "Last Failed", align: "left" },
{ key: "last_error", label: "Last Error", 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,
},
{
tooltip: "Run",
icon: <PlayArrowIcon />,
onClick: handleBatchRunClick,
disabled: props.batchActionPending,
},
]}
menuItemActions={[
{
label: "Delete All",
onClick: handleDeleteAllClick,
disabled: props.allActionPending,
},
{
label: "Run All",
onClick: handleRunAllClick,
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));
}
}}
onRunClick={() => {
props.runArchivedTaskAsync(queue, task.id);
}}
onDeleteClick={() => {
props.deleteArchivedTaskAsync(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;
onRunClick: () => void;
onDeleteClick: () => void;
allActionPending: boolean;
showActions: boolean;
onActionCellEnter: () => void;
onActionCellLeave: () => void;
}
const columns: TableColumn[] = [
{ key: "id", label: "ID", align: "left" },
{ key: "type", label: "Type", align: "left" },
{ key: "payload", label: "Payload", align: "left" },
{ key: "last_failed", label: "Last Failed", align: "left" },
{ key: "last_error", label: "Last Error", align: "left" },
{ key: "actions", label: "Actions", align: "center" },
];
function Row(props: RowProps) {
const { task } = props;
@ -333,18 +79,34 @@ function Row(props: RowProps) {
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)}
{!window.READ_ONLY && (
<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" className={classes.idCell}>
<div className={classes.IdGroup}>
{uuidPrefix(task.id)}
<Tooltip title="Copy full ID to clipboard">
<IconButton
onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(task.id);
}}
size="small"
className={classes.copyButton}
>
<FileCopyOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
</div>
</TableCell>
<TableCell>{task.type}</TableCell>
<TableCell>
@ -357,44 +119,57 @@ function Row(props: RowProps) {
</TableCell>
<TableCell>{timeAgo(task.last_failed_at)}</TableCell>
<TableCell>{task.error_message}</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>
<Tooltip title="Run">
<IconButton
className={classes.actionButton}
onClick={props.onRunClick}
disabled={task.requestPending || props.allActionPending}
size="small"
>
<PlayArrowIcon fontSize="small" />
</IconButton>
</Tooltip>
</React.Fragment>
) : (
<IconButton size="small" onClick={props.onActionCellEnter}>
<MoreHorizIcon fontSize="small" />
</IconButton>
)}
</TableCell>
{!window.READ_ONLY && (
<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>
<Tooltip title="Run">
<IconButton
className={classes.actionButton}
onClick={props.onRunClick}
disabled={task.requestPending || props.allActionPending}
size="small"
>
<PlayArrowIcon fontSize="small" />
</IconButton>
</Tooltip>
</React.Fragment>
) : (
<IconButton size="small" onClick={props.onActionCellEnter}>
<MoreHorizIcon fontSize="small" />
</IconButton>
)}
</TableCell>
)}
</TableRow>
);
}
function ArchivedTasksTable(props: Props & ReduxProps) {
return (
<TasksTable
taskState="archived"
columns={columns}
renderRow={(rowProps: RowProps) => <Row {...rowProps} />}
{...props}
/>
);
}
export default connector(ArchivedTasksTable);

View File

@ -1,63 +1,33 @@
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 IconButton from "@material-ui/core/IconButton";
import TableCell from "@material-ui/core/TableCell";
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 FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined";
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 React from "react";
import { connect, ConnectedProps } from "react-redux";
import { useHistory } from "react-router-dom";
import { taskRowsPerPageChange } from "../actions/settingsActions";
import {
listCompletedTasksAsync,
batchDeleteCompletedTasksAsync,
deleteAllCompletedTasksAsync,
deleteCompletedTaskAsync,
batchDeleteCompletedTasksAsync,
listCompletedTasksAsync,
} from "../actions/tasksActions";
import TablePaginationActions, {
rowsPerPageOptions,
} from "./TablePaginationActions";
import { taskRowsPerPageChange } from "../actions/settingsActions";
import TableActions from "./TableActions";
import { taskDetailsPath } from "../paths";
import { AppState } from "../store";
import { TableColumn } from "../types/table";
import {
durationFromSeconds,
prettifyPayload,
stringifyDuration,
timeAgo,
uuidPrefix,
prettifyPayload
} 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",
},
}));
import SyntaxHighlighter from "./SyntaxHighlighter";
import TasksTable, { RowProps, useRowStyles } from "./TasksTable";
function mapStateToProps(state: AppState) {
return {
@ -72,10 +42,10 @@ function mapStateToProps(state: AppState) {
}
const mapDispatchToProps = {
listCompletedTasksAsync,
deleteCompletedTaskAsync,
deleteAllCompletedTasksAsync,
batchDeleteCompletedTasksAsync,
listTasks: listCompletedTasksAsync,
deleteTask: deleteCompletedTaskAsync,
deleteAllTasks: deleteAllCompletedTasksAsync,
batchDeleteTasks: batchDeleteCompletedTasksAsync,
taskRowsPerPageChange,
};
@ -88,214 +58,15 @@ interface Props {
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;
}
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" },
];
function Row(props: RowProps) {
const { task } = props;
@ -308,18 +79,34 @@ function Row(props: RowProps) {
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)}
{!window.READ_ONLY && (
<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" className={classes.idCell}>
<div className={classes.IdGroup}>
{uuidPrefix(task.id)}
<Tooltip title="Copy full ID to clipboard">
<IconButton
onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(task.id);
}}
size="small"
className={classes.copyButton}
>
<FileCopyOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
</div>
</TableCell>
<TableCell>{task.type}</TableCell>
<TableCell>
@ -344,34 +131,46 @@ function Row(props: RowProps) {
? `${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>
{!window.READ_ONLY && (
<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>
);
}
function CompletedTasksTable(props: Props & ReduxProps) {
return (
<TasksTable
taskState="completed"
columns={columns}
renderRow={(rowProps: RowProps) => <Row {...rowProps} />}
{...props}
/>
);
}
export default connector(CompletedTasksTable);

View File

@ -0,0 +1,160 @@
import React from "react";
import { makeStyles, useTheme } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import Autocomplete from "@material-ui/lab/Autocomplete";
import useMediaQuery from "@material-ui/core/useMediaQuery";
import ListSubheader from "@material-ui/core/ListSubheader";
import { VariableSizeList, ListChildComponentProps } from "react-window";
import { GroupInfo } from "../api";
import { isDarkTheme } from "../theme";
const useStyles = makeStyles((theme) => ({
groupSelectOption: {
display: "flex",
justifyContent: "space-between",
width: "100%",
},
groupSize: {
fontSize: "12px",
color: theme.palette.text.secondary,
background: isDarkTheme(theme)
? "#303030"
: theme.palette.background.default,
textAlign: "center",
padding: "3px 6px",
borderRadius: "10px",
marginRight: "2px",
},
inputRoot: {
borderRadius: 20,
paddingLeft: "12px !important",
},
}));
interface Props {
selected: GroupInfo | null;
onSelect: (newVal: GroupInfo | null) => void;
groups: GroupInfo[];
error: string;
}
export default function GroupSelect(props: Props) {
const classes = useStyles();
const [inputValue, setInputValue] = React.useState("");
return (
<Autocomplete
id="task-group-selector"
value={props.selected}
onChange={(event: any, newValue: GroupInfo | null) => {
props.onSelect(newValue);
}}
inputValue={inputValue}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);
}}
disableListWrap
ListboxComponent={
ListboxComponent as React.ComponentType<
React.HTMLAttributes<HTMLElement>
>
}
options={props.groups}
getOptionLabel={(option: GroupInfo) => option.group}
style={{ width: 300 }}
renderOption={(option: GroupInfo) => (
<div className={classes.groupSelectOption}>
<span>{option.group}</span>
<span className={classes.groupSize}>{option.size}</span>
</div>
)}
renderInput={(params) => (
<TextField {...params} label="Select group" variant="outlined" />
)}
classes={{
inputRoot: classes.inputRoot,
}}
size="small"
/>
);
}
// Virtualized list.
// Reference: https://v4.mui.com/components/autocomplete/#virtualization
const LISTBOX_PADDING = 8; // px
function renderRow(props: ListChildComponentProps) {
const { data, index, style } = props;
return React.cloneElement(data[index], {
style: {
...style,
top: (style.top as number) + LISTBOX_PADDING,
},
});
}
const OuterElementContext = React.createContext({});
const OuterElementType = React.forwardRef<HTMLDivElement>((props, ref) => {
const outerProps = React.useContext(OuterElementContext);
return <div ref={ref} {...props} {...outerProps} />;
});
function useResetCache(data: any) {
const ref = React.useRef<VariableSizeList>(null);
React.useEffect(() => {
if (ref.current != null) {
ref.current.resetAfterIndex(0, true);
}
}, [data]);
return ref;
}
// Adapter for react-window
const ListboxComponent = React.forwardRef<HTMLDivElement>(
function ListboxComponent(props, ref) {
const { children, ...other } = props;
const itemData = React.Children.toArray(children);
const theme = useTheme();
const smUp = useMediaQuery(theme.breakpoints.up("sm"), { noSsr: true });
const itemCount = itemData.length;
const itemSize = smUp ? 36 : 48;
const getChildSize = (child: React.ReactNode) => {
if (React.isValidElement(child) && child.type === ListSubheader) {
return 48;
}
return itemSize;
};
const getHeight = () => {
if (itemCount > 8) {
return 8 * itemSize;
}
return itemData.map(getChildSize).reduce((a, b) => a + b, 0);
};
const gridRef = useResetCache(itemCount);
return (
<div ref={ref}>
<OuterElementContext.Provider value={other}>
<VariableSizeList
itemData={itemData}
height={getHeight() + 2 * LISTBOX_PADDING}
width="100%"
ref={gridRef}
outerElementType={OuterElementType}
innerElementType="ul"
itemSize={(index) => getChildSize(itemData[index])}
overscanCount={5}
itemCount={itemCount}
>
{renderRow}
</VariableSizeList>
</OuterElementContext.Provider>
</div>
);
}
);

View File

@ -1,61 +1,31 @@
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 Alert from "@material-ui/lab/Alert";
import AlertTitle from "@material-ui/lab/AlertTitle";
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 Checkbox from "@material-ui/core/Checkbox";
import IconButton from "@material-ui/core/IconButton";
import TableFooter from "@material-ui/core/TableFooter";
import TablePagination from "@material-ui/core/TablePagination";
import DeleteIcon from "@material-ui/icons/Delete";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import Tooltip from "@material-ui/core/Tooltip";
import ArchiveIcon from "@material-ui/icons/Archive";
import DeleteIcon from "@material-ui/icons/Delete";
import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined";
import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
import SyntaxHighlighter from "./SyntaxHighlighter";
import TablePaginationActions, {
rowsPerPageOptions,
} from "./TablePaginationActions";
import TableActions from "./TableActions";
import React from "react";
import { connect, ConnectedProps } from "react-redux";
import { useHistory } from "react-router-dom";
import { taskRowsPerPageChange } from "../actions/settingsActions";
import {
listPendingTasksAsync,
deletePendingTaskAsync,
batchDeletePendingTasksAsync,
deleteAllPendingTasksAsync,
archiveAllPendingTasksAsync,
archivePendingTaskAsync,
batchArchivePendingTasksAsync,
archiveAllPendingTasksAsync,
batchDeletePendingTasksAsync,
deleteAllPendingTasksAsync,
deletePendingTaskAsync,
listPendingTasksAsync,
} from "../actions/tasksActions";
import { taskRowsPerPageChange } from "../actions/settingsActions";
import { AppState } from "../store";
import { usePolling } from "../hooks";
import { uuidPrefix, prettifyPayload } from "../utils";
import { TableColumn } from "../types/table";
import { TaskInfoExtended } from "../reducers/tasksReducer";
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",
},
}));
import { AppState } from "../store";
import { TableColumn } from "../types/table";
import { prettifyPayload, uuidPrefix } from "../utils";
import SyntaxHighlighter from "./SyntaxHighlighter";
import TasksTable, { RowProps, useRowStyles } from "./TasksTable";
function mapStateToProps(state: AppState) {
return {
@ -70,13 +40,13 @@ function mapStateToProps(state: AppState) {
}
const mapDispatchToProps = {
listPendingTasksAsync,
deletePendingTaskAsync,
batchDeletePendingTasksAsync,
deleteAllPendingTasksAsync,
archivePendingTaskAsync,
batchArchivePendingTasksAsync,
archiveAllPendingTasksAsync,
listTasks: listPendingTasksAsync,
deleteTask: deletePendingTaskAsync,
batchDeleteTasks: batchDeletePendingTasksAsync,
deleteAllTasks: deleteAllPendingTasksAsync,
archiveTask: archivePendingTaskAsync,
batchArchiveTasks: batchArchivePendingTasksAsync,
archiveAllTasks: archiveAllPendingTasksAsync,
taskRowsPerPageChange,
};
@ -89,240 +59,14 @@ interface Props {
totalTaskCount: number; // total number of pending tasks
}
function PendingTasksTable(props: Props & ReduxProps) {
const { pollInterval, listPendingTasksAsync, 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.deleteAllPendingTasksAsync(queue);
};
const handleArchiveAllClick = () => {
props.archiveAllPendingTasksAsync(queue);
};
const handleBatchDeleteClick = () => {
props
.batchDeletePendingTasksAsync(queue, selectedIds)
.then(() => setSelectedIds([]));
};
const handleBatchArchiveClick = () => {
props
.batchArchivePendingTasksAsync(queue, selectedIds)
.then(() => setSelectedIds([]));
};
const fetchData = useCallback(() => {
const pageOpts = { page: page + 1, size: pageSize };
listPendingTasksAsync(queue, pageOpts);
}, [page, pageSize, queue, listPendingTasksAsync]);
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 pending tasks at this time.
</Alert>
);
}
const columns: TableColumn[] = [
{ key: "id", label: "ID", align: "left" },
{ key: "type", label: "Type", align: "left" },
{ key: "paylod", label: "Payload", align: "left" },
{ key: "retried", label: "Retried", align: "right" },
{ key: "max_retry", label: "Max Retry", align: "right" },
{ 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,
},
{
tooltip: "Archive",
icon: <ArchiveIcon />,
onClick: handleBatchArchiveClick,
disabled: props.batchActionPending,
},
]}
menuItemActions={[
{
label: "Delete All",
onClick: handleDeleteAllClick,
disabled: props.allActionPending,
},
{
label: "Archive All",
onClick: handleArchiveAllClick,
disabled: props.allActionPending,
},
]}
/>
<TableContainer component={Paper}>
<Table
stickyHeader={true}
className={classes.table}
aria-label="pending 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));
}
}}
allActionPending={props.allActionPending}
onDeleteClick={() =>
props.deletePendingTaskAsync(queue, task.id)
}
onArchiveClick={() => {
props.archivePendingTaskAsync(queue, task.id);
}}
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;
onArchiveClick: () => void;
allActionPending: boolean;
showActions: boolean;
onActionCellEnter: () => void;
onActionCellLeave: () => void;
}
const columns: TableColumn[] = [
{ key: "id", label: "ID", align: "left" },
{ key: "type", label: "Type", align: "left" },
{ key: "paylod", label: "Payload", align: "left" },
{ key: "retried", label: "Retried", align: "right" },
{ key: "max_retry", label: "Max Retry", align: "right" },
{ key: "actions", label: "Actions", align: "center" },
];
function Row(props: RowProps) {
const { task } = props;
@ -335,18 +79,34 @@ function Row(props: RowProps) {
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)}
{!window.READ_ONLY && (
<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" className={classes.idCell}>
<div className={classes.IdGroup}>
{uuidPrefix(task.id)}
<Tooltip title="Copy full ID to clipboard">
<IconButton
onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(task.id);
}}
size="small"
className={classes.copyButton}
>
<FileCopyOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
</div>
</TableCell>
<TableCell>{task.type}</TableCell>
<TableCell>
@ -359,44 +119,57 @@ function Row(props: RowProps) {
</TableCell>
<TableCell align="right">{task.retried}</TableCell>
<TableCell align="right">{task.max_retry}</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
onClick={props.onDeleteClick}
disabled={task.requestPending || props.allActionPending}
size="small"
className={classes.actionButton}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Archive">
<IconButton
onClick={props.onArchiveClick}
disabled={task.requestPending || props.allActionPending}
size="small"
className={classes.actionButton}
>
<ArchiveIcon fontSize="small" />
</IconButton>
</Tooltip>
</React.Fragment>
) : (
<IconButton size="small" onClick={props.onActionCellEnter}>
<MoreHorizIcon fontSize="small" />
</IconButton>
)}
</TableCell>
{!window.READ_ONLY && (
<TableCell
align="center"
className={classes.actionCell}
onMouseEnter={props.onActionCellEnter}
onMouseLeave={props.onActionCellLeave}
onClick={(e) => e.stopPropagation()}
>
{props.showActions ? (
<React.Fragment>
<Tooltip title="Delete">
<IconButton
onClick={props.onDeleteClick}
disabled={task.requestPending || props.allActionPending}
size="small"
className={classes.actionButton}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Archive">
<IconButton
onClick={props.onArchiveClick}
disabled={task.requestPending || props.allActionPending}
size="small"
className={classes.actionButton}
>
<ArchiveIcon fontSize="small" />
</IconButton>
</Tooltip>
</React.Fragment>
) : (
<IconButton size="small" onClick={props.onActionCellEnter}>
<MoreHorizIcon fontSize="small" />
</IconButton>
)}
</TableCell>
)}
</TableRow>
);
}
function PendingTasksTable(props: Props & ReduxProps) {
return (
<TasksTable
taskState="pending"
columns={columns}
renderRow={(rowProps: RowProps) => <Row {...rowProps} />}
{...props}
/>
);
}
export default connector(PendingTasksTable);

View File

@ -6,7 +6,7 @@ import Chip from "@material-ui/core/Chip";
import Menu from "@material-ui/core/Menu";
import MenuItem from "@material-ui/core/MenuItem";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import { paths, queueDetailsPath } from "../paths";
import { paths as getPaths, queueDetailsPath } from "../paths";
import { isDarkTheme } from "../theme";
const StyledBreadcrumb = withStyles((theme: Theme) => ({
@ -16,7 +16,7 @@ const StyledBreadcrumb = withStyles((theme: Theme) => ({
: theme.palette.background.default,
height: theme.spacing(3),
color: theme.palette.text.secondary,
fontWeight: theme.typography.fontWeightRegular,
fontWeight: 400,
"&:hover, &:focus": {
backgroundColor: theme.palette.action.hover,
},
@ -39,6 +39,7 @@ interface Props {
export default function QueueBreadcrumbs(props: Props) {
const history = useHistory();
const [anchorEl, setAnchorEl] = useState<null | Element>(null);
const paths = getPaths();
const handleClick = (event: React.MouseEvent<Element, MouseEvent>) => {
event.preventDefault();

View File

@ -65,6 +65,15 @@ function QueueInfoBanner(props: Props & ReduxProps) {
</Typography>
</div>
<div className={classes.bannerItem}>
<Typography variant="subtitle2" color="textPrimary" gutterBottom>
Task groups
</Typography>
<Typography color="textSecondary">
{queue ? queue.groups : "-"}
</Typography>
</div>
<div className={classes.bannerItem}>
<Typography variant="subtitle2" color="textPrimary" gutterBottom>
Memory usage
@ -74,6 +83,15 @@ function QueueInfoBanner(props: Props & ReduxProps) {
</Typography>
</div>
<div className={classes.bannerItem}>
<Typography variant="subtitle2" color="textPrimary" gutterBottom>
Latency
</Typography>
<Typography color="textSecondary">
{queue ? queue.display_latency : "-"}
</Typography>
</div>
<div className={classes.bannerItem}>
<Typography variant="subtitle2" color="textPrimary" gutterBottom>
Processed

View File

@ -21,6 +21,7 @@ interface TaskBreakdown {
queue: string; // name of the queue.
active: number; // number of active tasks in the queue.
pending: number; // number of pending tasks in the queue.
aggregating: number; // number of aggregating tasks in the queue.
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.
@ -55,6 +56,7 @@ function QueueSizeChart(props: Props) {
<Legend />
<Bar dataKey="active" stackId="a" fill="#1967d2" />
<Bar dataKey="pending" stackId="a" fill="#669df6" />
<Bar dataKey="aggregating" stackId="a" fill="#e69138" />
<Bar dataKey="scheduled" stackId="a" fill="#fdd663" />
<Bar dataKey="retry" stackId="a" fill="#f666a9" />
<Bar dataKey="archived" stackId="a" fill="#ac4776" />

View File

@ -50,6 +50,7 @@ enum SortBy {
State,
Size,
MemoryUsage,
Latency,
Processed,
Failed,
ErrorRate,
@ -72,6 +73,12 @@ const colConfigs: SortableTableColumn<SortBy>[] = [
sortBy: SortBy.MemoryUsage,
align: "right",
},
{
label: "Latency",
key: "latency",
sortBy: SortBy.Latency,
align: "right",
},
{
label: "Processed",
key: "processed",
@ -137,6 +144,10 @@ export default function QueuesOverviewTable(props: Props) {
if (q1.memory_usage_bytes === q2.memory_usage_bytes) return 0;
isQ1Smaller = q1.memory_usage_bytes < q2.memory_usage_bytes;
break;
case SortBy.Latency:
if (q1.latency_msec === q2.latency_msec) return 0;
isQ1Smaller = q1.latency_msec < q2.latency_msec;
break;
case SortBy.Processed:
if (q1.processed === q2.processed) return 0;
isQ1Smaller = q1.processed < q2.processed;
@ -172,25 +183,30 @@ export default function QueuesOverviewTable(props: Props) {
<Table className={classes.table} aria-label="queues overview table">
<TableHead>
<TableRow>
{colConfigs.map((cfg, i) => (
<TableCell
key={cfg.key}
align={cfg.align}
className={clsx(i === 0 && classes.fixedCell)}
>
{cfg.sortBy !== SortBy.None ? (
<TableSortLabel
active={sortBy === cfg.sortBy}
direction={sortDir}
onClick={createSortClickHandler(cfg.sortBy)}
>
{cfg.label}
</TableSortLabel>
) : (
<div>{cfg.label}</div>
)}
</TableCell>
))}
{colConfigs
.filter((cfg) => {
// Filter out actions column in readonly mode.
return !window.READ_ONLY || cfg.key !== "actions";
})
.map((cfg, i) => (
<TableCell
key={cfg.key}
align={cfg.align}
className={clsx(i === 0 && classes.fixedCell)}
>
{cfg.sortBy !== SortBy.None ? (
<TableSortLabel
active={sortBy === cfg.sortBy}
direction={sortDir}
onClick={createSortClickHandler(cfg.sortBy)}
>
{cfg.label}
</TableSortLabel>
) : (
<div>{cfg.label}</div>
)}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
@ -283,53 +299,56 @@ function Row(props: RowProps) {
</TableCell>
<TableCell align="right">{q.size}</TableCell>
<TableCell align="right">{prettyBytes(q.memory_usage_bytes)}</TableCell>
<TableCell align="right">{q.display_latency}</TableCell>
<TableCell align="right">{q.processed}</TableCell>
<TableCell align="right">{q.failed}</TableCell>
<TableCell align="right">{percentage(q.failed, q.processed)}</TableCell>
<TableCell
align="center"
onMouseEnter={() => setShowIcons(true)}
onMouseLeave={() => setShowIcons(false)}
>
<div className={classes.actionIconsContainer}>
{showIcons ? (
<React.Fragment>
{q.paused ? (
<Tooltip title="Resume">
<IconButton
color="secondary"
onClick={props.onResumeClick}
disabled={q.requestPending}
size="small"
>
<PlayCircleFilledIcon fontSize="small" />
{!window.READ_ONLY && (
<TableCell
align="center"
onMouseEnter={() => setShowIcons(true)}
onMouseLeave={() => setShowIcons(false)}
>
<div className={classes.actionIconsContainer}>
{showIcons ? (
<React.Fragment>
{q.paused ? (
<Tooltip title="Resume">
<IconButton
color="secondary"
onClick={props.onResumeClick}
disabled={q.requestPending}
size="small"
>
<PlayCircleFilledIcon fontSize="small" />
</IconButton>
</Tooltip>
) : (
<Tooltip title="Pause">
<IconButton
color="primary"
onClick={props.onPauseClick}
disabled={q.requestPending}
size="small"
>
<PauseCircleFilledIcon fontSize="small" />
</IconButton>
</Tooltip>
)}
<Tooltip title="Delete">
<IconButton onClick={props.onDeleteClick} size="small">
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
) : (
<Tooltip title="Pause">
<IconButton
color="primary"
onClick={props.onPauseClick}
disabled={q.requestPending}
size="small"
>
<PauseCircleFilledIcon fontSize="small" />
</IconButton>
</Tooltip>
)}
<Tooltip title="Delete">
<IconButton onClick={props.onDeleteClick} size="small">
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
</React.Fragment>
) : (
<IconButton size="small">
<MoreHorizIcon fontSize="small" />
</IconButton>
)}
</div>
</TableCell>
</React.Fragment>
) : (
<IconButton size="small">
<MoreHorizIcon fontSize="small" />
</IconButton>
)}
</div>
</TableCell>
)}
</TableRow>
);
}

View File

@ -1,65 +1,35 @@
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 TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableFooter from "@material-ui/core/TableFooter";
import TablePagination from "@material-ui/core/TablePagination";
import Paper from "@material-ui/core/Paper";
import Tooltip from "@material-ui/core/Tooltip";
import Checkbox from "@material-ui/core/Checkbox";
import IconButton from "@material-ui/core/IconButton";
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import DeleteIcon from "@material-ui/icons/Delete";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import Tooltip from "@material-ui/core/Tooltip";
import ArchiveIcon from "@material-ui/icons/Archive";
import DeleteIcon from "@material-ui/icons/Delete";
import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined";
import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
import Alert from "@material-ui/lab/Alert";
import AlertTitle from "@material-ui/lab/AlertTitle";
import SyntaxHighlighter from "./SyntaxHighlighter";
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import React from "react";
import { connect, ConnectedProps } from "react-redux";
import { useHistory } from "react-router-dom";
import { taskRowsPerPageChange } from "../actions/settingsActions";
import {
archiveAllRetryTasksAsync,
archiveRetryTaskAsync,
batchArchiveRetryTasksAsync,
batchDeleteRetryTasksAsync,
batchRunRetryTasksAsync,
batchArchiveRetryTasksAsync,
deleteAllRetryTasksAsync,
runAllRetryTasksAsync,
archiveAllRetryTasksAsync,
listRetryTasksAsync,
deleteRetryTaskAsync,
listRetryTasksAsync,
runAllRetryTasksAsync,
runRetryTaskAsync,
archiveRetryTaskAsync,
} from "../actions/tasksActions";
import { AppState } from "../store";
import TablePaginationActions, {
rowsPerPageOptions,
} from "./TablePaginationActions";
import { taskRowsPerPageChange } from "../actions/settingsActions";
import TableActions from "./TableActions";
import { durationBefore, uuidPrefix, prettifyPayload } from "../utils";
import { usePolling } from "../hooks";
import { TaskInfoExtended } from "../reducers/tasksReducer";
import { TableColumn } from "../types/table";
import TasksTable, { RowProps, useRowStyles } from "./TasksTable";
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",
},
}));
import { AppState } from "../store";
import { TableColumn } from "../types/table";
import { durationBefore, prettifyPayload, uuidPrefix } from "../utils";
import SyntaxHighlighter from "./SyntaxHighlighter";
function mapStateToProps(state: AppState) {
return {
@ -74,16 +44,16 @@ function mapStateToProps(state: AppState) {
}
const mapDispatchToProps = {
batchDeleteRetryTasksAsync,
batchRunRetryTasksAsync,
batchArchiveRetryTasksAsync,
deleteAllRetryTasksAsync,
runAllRetryTasksAsync,
archiveAllRetryTasksAsync,
listRetryTasksAsync,
deleteRetryTaskAsync,
runRetryTaskAsync,
archiveRetryTaskAsync,
batchDeleteTasks: batchDeleteRetryTasksAsync,
batchRunTasks: batchRunRetryTasksAsync,
batchArchiveTasks: batchArchiveRetryTasksAsync,
deleteAllTasks: deleteAllRetryTasksAsync,
runAllTasks: runAllRetryTasksAsync,
archiveAllTasks: archiveAllRetryTasksAsync,
listTasks: listRetryTasksAsync,
deleteTask: deleteRetryTaskAsync,
runTask: runRetryTaskAsync,
archiveTask: archiveRetryTaskAsync,
taskRowsPerPageChange,
};
@ -96,265 +66,16 @@ interface Props {
totalTaskCount: number; // totoal number of scheduled tasks.
}
function RetryTasksTable(props: Props & ReduxProps) {
const { pollInterval, listRetryTasksAsync, 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 handleRunAllClick = () => {
props.runAllRetryTasksAsync(queue);
};
const handleDeleteAllClick = () => {
props.deleteAllRetryTasksAsync(queue);
};
const handleArchiveAllClick = () => {
props.archiveAllRetryTasksAsync(queue);
};
const handleBatchRunClick = () => {
props
.batchRunRetryTasksAsync(queue, selectedIds)
.then(() => setSelectedIds([]));
};
const handleBatchDeleteClick = () => {
props
.batchDeleteRetryTasksAsync(queue, selectedIds)
.then(() => setSelectedIds([]));
};
const handleBatchArchiveClick = () => {
props
.batchArchiveRetryTasksAsync(queue, selectedIds)
.then(() => setSelectedIds([]));
};
const fetchData = useCallback(() => {
const pageOpts = { page: page + 1, size: pageSize };
listRetryTasksAsync(queue, pageOpts);
}, [page, pageSize, queue, listRetryTasksAsync]);
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 retry 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: "retry_in", label: "Retry In", align: "left" },
{ key: "last_error", label: "Last Error", align: "left" },
{ key: "retried", label: "Retried", align: "right" },
{ key: "max_retry", label: "Max Retry", align: "right" },
{ 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,
},
{
tooltip: "Archive",
icon: <ArchiveIcon />,
onClick: handleBatchArchiveClick,
disabled: props.batchActionPending,
},
{
tooltip: "Run",
icon: <PlayArrowIcon />,
onClick: handleBatchRunClick,
disabled: props.batchActionPending,
},
]}
menuItemActions={[
{
label: "Delete All",
onClick: handleDeleteAllClick,
disabled: props.allActionPending,
},
{
label: "Archive All",
onClick: handleArchiveAllClick,
disabled: props.allActionPending,
},
{
label: "Run All",
onClick: handleRunAllClick,
disabled: props.allActionPending,
},
]}
/>
<TableContainer component={Paper}>
<Table
stickyHeader={true}
className={classes.table}
aria-label="retry 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.label}
align={col.align}
classes={{ stickyHeader: classes.stickyHeaderCell }}
>
{col.label}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{props.tasks.map((task) => (
<Row
key={task.id}
task={task}
allActionPending={props.allActionPending}
isSelected={selectedIds.includes(task.id)}
onSelectChange={(checked: boolean) => {
if (checked) {
setSelectedIds(selectedIds.concat(task.id));
} else {
setSelectedIds(selectedIds.filter((id) => id !== task.id));
}
}}
onRunClick={() => {
props.runRetryTaskAsync(task.queue, task.id);
}}
onDeleteClick={() => {
props.deleteRetryTaskAsync(task.queue, task.id);
}}
onArchiveClick={() => {
props.archiveRetryTaskAsync(task.queue, task.id);
}}
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: "140px",
},
actionButton: {
marginLeft: 3,
marginRight: 3,
},
}));
interface RowProps {
task: TaskInfoExtended;
isSelected: boolean;
onSelectChange: (checked: boolean) => void;
onDeleteClick: () => void;
onRunClick: () => void;
onArchiveClick: () => void;
allActionPending: boolean;
showActions: boolean;
onActionCellEnter: () => void;
onActionCellLeave: () => void;
}
const columns: TableColumn[] = [
{ key: "id", label: "ID", align: "left" },
{ key: "type", label: "Type", align: "left" },
{ key: "payload", label: "Payload", align: "left" },
{ key: "retry_in", label: "Retry In", align: "left" },
{ key: "last_error", label: "Last Error", align: "left" },
{ key: "retried", label: "Retried", align: "right" },
{ key: "max_retry", label: "Max Retry", align: "right" },
{ key: "actions", label: "Actions", align: "center" },
];
function Row(props: RowProps) {
const { task } = props;
@ -368,18 +89,34 @@ function Row(props: RowProps) {
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)}
{!window.READ_ONLY && (
<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" className={classes.idCell}>
<div className={classes.IdGroup}>
{uuidPrefix(task.id)}
<Tooltip title="Copy full ID to clipboard">
<IconButton
onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(task.id);
}}
size="small"
className={classes.copyButton}
>
<FileCopyOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
</div>
</TableCell>
<TableCell>{task.type}</TableCell>
<TableCell>
@ -394,54 +131,67 @@ function Row(props: RowProps) {
<TableCell>{task.error_message}</TableCell>
<TableCell align="right">{task.retried}</TableCell>
<TableCell align="right">{task.max_retry}</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
onClick={props.onDeleteClick}
disabled={task.requestPending || props.allActionPending}
size="small"
className={classes.actionButton}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Archive">
<IconButton
onClick={props.onArchiveClick}
disabled={task.requestPending || props.allActionPending}
size="small"
className={classes.actionButton}
>
<ArchiveIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Run">
<IconButton
onClick={props.onRunClick}
disabled={task.requestPending || props.allActionPending}
size="small"
className={classes.actionButton}
>
<PlayArrowIcon fontSize="small" />
</IconButton>
</Tooltip>
</React.Fragment>
) : (
<IconButton size="small" onClick={props.onActionCellEnter}>
<MoreHorizIcon fontSize="small" />
</IconButton>
)}
</TableCell>
{!window.READ_ONLY && (
<TableCell
align="center"
className={classes.actionCell}
onMouseEnter={props.onActionCellEnter}
onMouseLeave={props.onActionCellLeave}
onClick={(e) => e.stopPropagation()}
>
{props.showActions ? (
<React.Fragment>
<Tooltip title="Delete">
<IconButton
onClick={props.onDeleteClick}
disabled={task.requestPending || props.allActionPending}
size="small"
className={classes.actionButton}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Archive">
<IconButton
onClick={props.onArchiveClick}
disabled={task.requestPending || props.allActionPending}
size="small"
className={classes.actionButton}
>
<ArchiveIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Run">
<IconButton
onClick={props.onRunClick}
disabled={task.requestPending || props.allActionPending}
size="small"
className={classes.actionButton}
>
<PlayArrowIcon fontSize="small" />
</IconButton>
</Tooltip>
</React.Fragment>
) : (
<IconButton size="small" onClick={props.onActionCellEnter}>
<MoreHorizIcon fontSize="small" />
</IconButton>
)}
</TableCell>
)}
</TableRow>
);
}
function RetryTasksTable(props: Props & ReduxProps) {
return (
<TasksTable
taskState="retry"
columns={columns}
renderRow={(rowProps: RowProps) => <Row {...rowProps} />}
{...props}
/>
);
}
export default connector(RetryTasksTable);

View File

@ -1,26 +1,18 @@
import React, { useState, useCallback } from "react";
import { useHistory } from "react-router-dom";
import React from "react";
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 TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import { useHistory } from "react-router-dom";
import TableRow from "@material-ui/core/TableRow";
import TableFooter from "@material-ui/core/TableFooter";
import TablePagination from "@material-ui/core/TablePagination";
import Paper from "@material-ui/core/Paper";
import Tooltip from "@material-ui/core/Tooltip";
import TableCell from "@material-ui/core/TableCell";
import Checkbox from "@material-ui/core/Checkbox";
import IconButton from "@material-ui/core/IconButton";
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import Tooltip from "@material-ui/core/Tooltip";
import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined";
import DeleteIcon from "@material-ui/icons/Delete";
import ArchiveIcon from "@material-ui/icons/Archive";
import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
import Alert from "@material-ui/lab/Alert";
import AlertTitle from "@material-ui/lab/AlertTitle";
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import SyntaxHighlighter from "./SyntaxHighlighter";
import TasksTable, { RowProps, useRowStyles } from "./TasksTable";
import {
batchDeleteScheduledTasksAsync,
batchRunScheduledTasksAsync,
@ -35,32 +27,10 @@ import {
} from "../actions/tasksActions";
import { taskRowsPerPageChange } from "../actions/settingsActions";
import { AppState } from "../store";
import TablePaginationActions, {
rowsPerPageOptions,
} from "./TablePaginationActions";
import TableActions from "./TableActions";
import { durationBefore, uuidPrefix, prettifyPayload } from "../utils";
import { usePolling } from "../hooks";
import { TaskInfoExtended } from "../reducers/tasksReducer";
import { TableColumn } from "../types/table";
import { durationBefore, prettifyPayload, uuidPrefix } from "../utils";
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.scheduledTasks.loading,
@ -74,16 +44,16 @@ function mapStateToProps(state: AppState) {
}
const mapDispatchToProps = {
listScheduledTasksAsync,
batchDeleteScheduledTasksAsync,
batchRunScheduledTasksAsync,
batchArchiveScheduledTasksAsync,
deleteAllScheduledTasksAsync,
runAllScheduledTasksAsync,
archiveAllScheduledTasksAsync,
deleteScheduledTaskAsync,
runScheduledTaskAsync,
archiveScheduledTaskAsync,
listTasks: listScheduledTasksAsync,
batchDeleteTasks: batchDeleteScheduledTasksAsync,
batchRunTasks: batchRunScheduledTasksAsync,
batchArchiveTasks: batchArchiveScheduledTasksAsync,
deleteAllTasks: deleteAllScheduledTasksAsync,
runAllTasks: runAllScheduledTasksAsync,
archiveAllTasks: archiveAllScheduledTasksAsync,
deleteTask: deleteScheduledTaskAsync,
runTask: runScheduledTaskAsync,
archiveTask: archiveScheduledTaskAsync,
taskRowsPerPageChange,
};
@ -96,262 +66,13 @@ interface Props {
totalTaskCount: number; // totoal number of scheduled tasks.
}
function ScheduledTasksTable(props: Props & ReduxProps) {
const { pollInterval, listScheduledTasksAsync, 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 handleRunAllClick = () => {
props.runAllScheduledTasksAsync(queue);
};
const handleDeleteAllClick = () => {
props.deleteAllScheduledTasksAsync(queue);
};
const handleArchiveAllClick = () => {
props.archiveAllScheduledTasksAsync(queue);
};
const handleBatchRunClick = () => {
props
.batchRunScheduledTasksAsync(queue, selectedIds)
.then(() => setSelectedIds([]));
};
const handleBatchDeleteClick = () => {
props
.batchDeleteScheduledTasksAsync(queue, selectedIds)
.then(() => setSelectedIds([]));
};
const handleBatchArchiveClick = () => {
props
.batchArchiveScheduledTasksAsync(queue, selectedIds)
.then(() => setSelectedIds([]));
};
const fetchData = useCallback(() => {
const pageOpts = { page: page + 1, size: pageSize };
listScheduledTasksAsync(queue, pageOpts);
}, [page, pageSize, queue, listScheduledTasksAsync]);
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 scheduled 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: "process_in", label: "Process In", 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,
},
{
tooltip: "Archive",
icon: <ArchiveIcon />,
onClick: handleBatchArchiveClick,
disabled: props.batchActionPending,
},
{
tooltip: "Run",
icon: <PlayArrowIcon />,
onClick: handleBatchRunClick,
disabled: props.batchActionPending,
},
]}
menuItemActions={[
{
label: "Delete All",
onClick: handleDeleteAllClick,
disabled: props.allActionPending,
},
{
label: "Archive All",
onClick: handleArchiveAllClick,
disabled: props.allActionPending,
},
{
label: "Run All",
onClick: handleRunAllClick,
disabled: props.allActionPending,
},
]}
/>
<TableContainer component={Paper}>
<Table
stickyHeader={true}
className={classes.table}
aria-label="scheduled 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.label}
align={col.align}
classes={{ stickyHeader: classes.stickyHeaderCell }}
>
{col.label}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{props.tasks.map((task) => (
<Row
key={task.id}
task={task}
allActionPending={props.allActionPending}
isSelected={selectedIds.includes(task.id)}
onSelectChange={(checked: boolean) => {
if (checked) {
setSelectedIds(selectedIds.concat(task.id));
} else {
setSelectedIds(selectedIds.filter((id) => id !== task.id));
}
}}
onRunClick={() => {
props.runScheduledTaskAsync(queue, task.id);
}}
onDeleteClick={() => {
props.deleteScheduledTaskAsync(queue, task.id);
}}
onArchiveClick={() => {
props.archiveScheduledTaskAsync(queue, task.id);
}}
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: "140px",
},
actionButton: {
marginLeft: 3,
marginRight: 3,
},
}));
interface RowProps {
task: TaskInfoExtended;
isSelected: boolean;
onSelectChange: (checked: boolean) => void;
onRunClick: () => void;
onDeleteClick: () => void;
onArchiveClick: () => void;
allActionPending: boolean;
showActions: boolean;
onActionCellEnter: () => void;
onActionCellLeave: () => void;
}
const columns: TableColumn[] = [
{ key: "id", label: "ID", align: "left" },
{ key: "type", label: "Type", align: "left" },
{ key: "payload", label: "Payload", align: "left" },
{ key: "process_in", label: "Process In", align: "left" },
{ key: "actions", label: "Actions", align: "center" },
];
function Row(props: RowProps) {
const { task } = props;
@ -364,18 +85,34 @@ function Row(props: RowProps) {
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)}
{!window.READ_ONLY && (
<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" className={classes.idCell}>
<div className={classes.IdGroup}>
{uuidPrefix(task.id)}
<Tooltip title="Copy full ID to clipboard">
<IconButton
onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(task.id);
}}
size="small"
className={classes.copyButton}
>
<FileCopyOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
</div>
</TableCell>
<TableCell>{task.type}</TableCell>
<TableCell>
@ -387,53 +124,67 @@ function Row(props: RowProps) {
</SyntaxHighlighter>
</TableCell>
<TableCell>{durationBefore(task.next_process_at)}</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
onClick={props.onDeleteClick}
disabled={task.requestPending || props.allActionPending}
size="small"
className={classes.actionButton}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Archive">
<IconButton
onClick={props.onArchiveClick}
disabled={task.requestPending || props.allActionPending}
size="small"
className={classes.actionButton}
>
<ArchiveIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Run">
<IconButton
onClick={props.onRunClick}
disabled={task.requestPending || props.allActionPending}
size="small"
className={classes.actionButton}
>
<PlayArrowIcon fontSize="small" />
</IconButton>
</Tooltip>
</React.Fragment>
) : (
<IconButton size="small" onClick={props.onActionCellEnter}>
<MoreHorizIcon fontSize="small" />
</IconButton>
)}
</TableCell>
{!window.READ_ONLY && (
<TableCell
align="center"
className={classes.actionCell}
onMouseEnter={props.onActionCellEnter}
onMouseLeave={props.onActionCellLeave}
onClick={(e) => e.stopPropagation()}
>
{props.showActions ? (
<React.Fragment>
<Tooltip title="Delete">
<IconButton
onClick={props.onDeleteClick}
disabled={task.requestPending || props.allActionPending}
size="small"
className={classes.actionButton}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Archive">
<IconButton
onClick={props.onArchiveClick}
disabled={task.requestPending || props.allActionPending}
size="small"
className={classes.actionButton}
>
<ArchiveIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Run">
<IconButton
onClick={props.onRunClick}
disabled={task.requestPending || props.allActionPending}
size="small"
className={classes.actionButton}
>
<PlayArrowIcon fontSize="small" />
</IconButton>
</Tooltip>
</React.Fragment>
) : (
<IconButton size="small" onClick={props.onActionCellEnter}>
<MoreHorizIcon fontSize="small" />
</IconButton>
)}
</TableCell>
)}
</TableRow>
);
}
function ScheduledTasksTable(props: Props & ReduxProps) {
return (
<TasksTable
taskState="scheduled"
columns={columns}
renderRow={(rowProps: RowProps) => <Row {...rowProps} />}
{...props}
/>
);
}
export default connector(ScheduledTasksTable);

View File

@ -1,6 +1,7 @@
import React from "react";
import { useTheme, Theme } from "@material-ui/core/styles";
import ReactSyntaxHighlighter from "react-syntax-highlighter";
import { Light as ReactSyntaxHighlighter } from "react-syntax-highlighter";
import json from "react-syntax-highlighter/dist/esm/languages/hljs/json";
import styleDark from "react-syntax-highlighter/dist/esm/styles/hljs/atom-one-dark";
import styleLight from "react-syntax-highlighter/dist/esm/styles/hljs/atom-one-light";
import { isDarkTheme } from "../theme";
@ -11,6 +12,8 @@ interface Props {
customStyle?: object;
}
ReactSyntaxHighlighter.registerLanguage("json", json);
// Theme aware syntax-highlighter component.
export default function SyntaxHighlighter(props: Props) {
const theme = useTheme<Theme>();

View File

@ -1,246 +1,378 @@
import React, { useState } from "react";
import { connect, ConnectedProps } from "react-redux";
import React, { useState, useCallback } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableFooter from "@material-ui/core/TableFooter";
import TablePagination from "@material-ui/core/TablePagination";
import Paper from "@material-ui/core/Paper";
import Chip from "@material-ui/core/Chip";
import InputBase from "@material-ui/core/InputBase";
import SearchIcon from "@material-ui/icons/Search";
import ActiveTasksTable from "./ActiveTasksTable";
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";
import { AppState } from "../store";
import { isDarkTheme } from "../theme";
import Checkbox from "@material-ui/core/Checkbox";
import IconButton from "@material-ui/core/IconButton";
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import DeleteIcon from "@material-ui/icons/Delete";
import ArchiveIcon from "@material-ui/icons/Archive";
import CancelIcon from "@material-ui/icons/Cancel";
import Alert from "@material-ui/lab/Alert";
import AlertTitle from "@material-ui/lab/AlertTitle";
import TablePaginationActions, {
rowsPerPageOptions,
} from "./TablePaginationActions";
import TableActions from "./TableActions";
import { usePolling } from "../hooks";
import { TaskInfoExtended } from "../reducers/tasksReducer";
import { TableColumn } from "../types/table";
import { PaginationOptions } from "../api";
import { TaskState } from "../types/taskState";
interface TabPanelProps {
children?: React.ReactNode;
selected: string; // currently selected value
value: string; // tab panel will be shown if selected value equals to the value
const useStyles = makeStyles((theme) => ({
table: {
minWidth: 650,
},
stickyHeaderCell: {
background: theme.palette.background.paper,
},
alert: {
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
},
pagination: {
border: "none",
},
}));
interface Props {
queue: string; // name of the queue.
totalTaskCount: number; // totoal number of tasks in the given state.
taskState: TaskState;
loading: boolean;
error: string;
tasks: TaskInfoExtended[];
batchActionPending: boolean;
allActionPending: boolean;
pollInterval: number;
pageSize: number;
columns: TableColumn[];
// actions
listTasks: (qname: string, pgn: PaginationOptions) => void;
batchDeleteTasks?: (qname: string, taskIds: string[]) => Promise<void>;
batchRunTasks?: (qname: string, taskIds: string[]) => Promise<void>;
batchArchiveTasks?: (qname: string, taskIds: string[]) => Promise<void>;
batchCancelTasks?: (qname: string, taskIds: string[]) => Promise<void>;
deleteAllTasks?: (qname: string) => Promise<void>;
runAllTasks?: (qname: string) => Promise<void>;
archiveAllTasks?: (qname: string) => Promise<void>;
cancelAllTasks?: (qname: string) => Promise<void>;
deleteTask?: (qname: string, taskId: string) => Promise<void>;
runTask?: (qname: string, taskId: string) => Promise<void>;
archiveTask?: (qname: string, taskId: string) => Promise<void>;
cancelTask?: (qname: string, taskId: string) => Promise<void>;
taskRowsPerPageChange: (n: number) => void;
renderRow: (rowProps: RowProps) => JSX.Element;
}
function TabPanel(props: TabPanelProps) {
const { children, value, selected, ...other } = props;
export default function TasksTable(props: Props) {
const { pollInterval, listTasks, 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([]);
}
};
function createAllActionHandler(action: (qname: string) => Promise<void>) {
return () => action(queue);
}
function createBatchActionHandler(
action: (qname: string, taskIds: string[]) => Promise<void>
) {
return () => action(queue, selectedIds).then(() => setSelectedIds([]));
}
function createSingleActionHandler(
action: (qname: string, taskId: string) => Promise<void>,
taskId: string
) {
return () => action(queue, taskId);
}
let allActions = [];
if (props.deleteAllTasks) {
allActions.push({
label: "Delete All",
onClick: createAllActionHandler(props.deleteAllTasks),
disabled: props.allActionPending,
});
}
if (props.archiveAllTasks) {
allActions.push({
label: "Archive All",
onClick: createAllActionHandler(props.archiveAllTasks),
disabled: props.allActionPending,
});
}
if (props.runAllTasks) {
allActions.push({
label: "Run All",
onClick: createAllActionHandler(props.runAllTasks),
disabled: props.allActionPending,
});
}
if (props.cancelAllTasks) {
allActions.push({
label: "Cancel All",
onClick: createAllActionHandler(props.cancelAllTasks),
disabled: props.allActionPending,
});
}
let batchActions = [];
if (props.batchDeleteTasks) {
batchActions.push({
tooltip: "Delete",
icon: <DeleteIcon />,
disabled: props.batchActionPending,
onClick: createBatchActionHandler(props.batchDeleteTasks),
});
}
if (props.batchArchiveTasks) {
batchActions.push({
tooltip: "Archive",
icon: <ArchiveIcon />,
disabled: props.batchActionPending,
onClick: createBatchActionHandler(props.batchArchiveTasks),
});
}
if (props.batchRunTasks) {
batchActions.push({
tooltip: "Run",
icon: <PlayArrowIcon />,
disabled: props.batchActionPending,
onClick: createBatchActionHandler(props.batchRunTasks),
});
}
if (props.batchCancelTasks) {
batchActions.push({
tooltip: "Cancel",
icon: <CancelIcon />,
disabled: props.batchActionPending,
onClick: createBatchActionHandler(props.batchCancelTasks),
});
}
const fetchData = useCallback(() => {
const pageOpts = { page: page + 1, size: pageSize };
listTasks(queue, pageOpts);
}, [page, pageSize, queue, listTasks]);
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>
{props.taskState === "aggregating" ? (
<div>Selected group is empty.</div>
) : (
<div>No {props.taskState} tasks at this time.</div>
)}
</Alert>
);
}
const rowCount = props.tasks.length;
const numSelected = selectedIds.length;
return (
<div
role="tabpanel"
hidden={value !== selected}
id={`scrollable-auto-tabpanel-${selected}`}
aria-labelledby={`scrollable-auto-tab-${selected}`}
style={{ flex: 1, overflowY: "scroll" }}
{...other}
>
{value === selected && children}
<div>
{!window.READ_ONLY && (
<TableActions
showIconButtons={numSelected > 0}
iconButtonActions={batchActions}
menuItemActions={allActions}
/>
)}
<TableContainer component={Paper}>
<Table
stickyHeader={true}
className={classes.table}
aria-label={`${props.taskState} tasks table`}
size="small"
>
<TableHead>
<TableRow>
{!window.READ_ONLY && (
<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>
)}
{props.columns
.filter((col) => {
// Filter out actions column in readonly mode.
return !window.READ_ONLY || col.key !== "actions";
})
.map((col) => (
<TableCell
key={col.label}
align={col.align}
classes={{ stickyHeader: classes.stickyHeaderCell }}
>
{col.label}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{props.tasks.map((task) => {
return props.renderRow({
key: task.id,
task: task,
allActionPending: props.allActionPending,
isSelected: selectedIds.includes(task.id),
onSelectChange: (checked: boolean) => {
if (checked) {
setSelectedIds(selectedIds.concat(task.id));
} else {
setSelectedIds(selectedIds.filter((id) => id !== task.id));
}
},
onRunClick: props.runTask
? createSingleActionHandler(props.runTask, task.id)
: undefined,
onDeleteClick: props.deleteTask
? createSingleActionHandler(props.deleteTask, task.id)
: undefined,
onArchiveClick: props.archiveTask
? createSingleActionHandler(props.archiveTask, task.id)
: undefined,
onCancelClick: props.cancelTask
? createSingleActionHandler(props.cancelTask, task.id)
: undefined,
onActionCellEnter: () => setActiveTaskId(task.id),
onActionCellLeave: () => setActiveTaskId(""),
showActions: activeTaskId === task.id,
});
})}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={rowsPerPageOptions}
colSpan={props.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>
);
}
function mapStatetoProps(state: AppState, ownProps: Props) {
// TODO: Add loading state for each queue.
const queueInfo = state.queues.data.find(
(q: QueueInfo) => q.name === ownProps.queue
);
const currentStats = queueInfo
? queueInfo.currentStats
: {
queue: ownProps.queue,
paused: false,
size: 0,
active: 0,
pending: 0,
scheduled: 0,
retry: 0,
archived: 0,
completed: 0,
processed: 0,
failed: 0,
timestamp: "n/a",
};
return { currentStats };
}
const connector = connect(mapStatetoProps);
type ReduxProps = ConnectedProps<typeof connector>;
interface Props {
queue: string;
selected: string;
}
const useStyles = makeStyles((theme) => ({
container: {
width: "100%",
height: "100%",
background: theme.palette.background.paper,
},
header: {
display: "flex",
alignItems: "center",
paddingTop: theme.spacing(1),
},
heading: {
paddingTop: theme.spacing(1),
paddingBottom: theme.spacing(1),
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
},
chip: {
marginLeft: theme.spacing(1),
},
taskcount: {
fontSize: "12px",
color: theme.palette.text.secondary,
background: isDarkTheme(theme)
? "#303030"
: theme.palette.background.default,
textAlign: "center",
padding: "3px 6px",
borderRadius: "10px",
marginLeft: "2px",
},
searchbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
},
search: {
position: "relative",
width: "312px",
borderRadius: "18px",
backgroundColor: isDarkTheme(theme) ? "#303030" : theme.palette.grey[100],
"&:hover, &:focus": {
backgroundColor: isDarkTheme(theme) ? "#303030" : theme.palette.grey[200],
export const useRowStyles = makeStyles((theme) => ({
root: {
cursor: "pointer",
"& #copy-button": {
display: "none",
},
"&:hover": {
boxShadow: theme.shadows[2],
"& #copy-button": {
display: "inline-block",
},
},
"&:hover $copyButton": {
display: "inline-block",
},
"&:hover .MuiTableCell-root": {
borderBottomColor: theme.palette.background.paper,
},
},
searchIcon: {
padding: theme.spacing(0, 2),
height: "100%",
position: "absolute",
pointerEvents: "none",
actionCell: {
width: "140px",
},
actionButton: {
marginLeft: 3,
marginRight: 3,
},
idCell: {
width: "200px",
},
copyButton: {
display: "none",
},
IdGroup: {
display: "flex",
alignItems: "center",
justifyContent: "center",
},
inputRoot: {
color: "inherit",
width: "100%",
},
inputInput: {
padding: theme.spacing(1, 1, 1, 0),
// vertical padding + font size from searchIcon
paddingLeft: `calc(1em + ${theme.spacing(4)}px)`,
width: "100%",
fontSize: "0.85rem",
},
}));
function TasksTable(props: Props & ReduxProps) {
const { currentStats } = props;
const classes = useStyles();
const history = useHistory();
const chips = [
{ key: "active", label: "Active", count: currentStats.active },
{ key: "pending", label: "Pending", count: currentStats.pending },
{ 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>("");
return (
<Paper variant="outlined" className={classes.container}>
<div className={classes.header}>
<Typography color="textPrimary" className={classes.heading}>
Tasks
</Typography>
<div>
{chips.map((c) => (
<Chip
key={c.key}
className={classes.chip}
label={
<div>
{c.label} <span className={classes.taskcount}>{c.count}</span>
</div>
}
variant="outlined"
color={props.selected === c.key ? "primary" : "default"}
onClick={() => history.push(queueDetailsPath(props.queue, c.key))}
/>
))}
</div>
<div className={classes.searchbar}>
<div className={classes.search}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
<InputBase
placeholder="Search by ID"
classes={{
root: classes.inputRoot,
input: classes.inputInput,
}}
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
}}
inputProps={{
"aria-label": "search",
onKeyDown: (e) => {
if (e.key === "Enter") {
history.push(
taskDetailsPath(props.queue, searchQuery.trim())
);
}
},
}}
/>
</div>
</div>
</div>
<TabPanel value="active" selected={props.selected}>
<ActiveTasksTable queue={props.queue} />
</TabPanel>
<TabPanel value="pending" selected={props.selected}>
<PendingTasksTable
queue={props.queue}
totalTaskCount={currentStats.pending}
/>
</TabPanel>
<TabPanel value="scheduled" selected={props.selected}>
<ScheduledTasksTable
queue={props.queue}
totalTaskCount={currentStats.scheduled}
/>
</TabPanel>
<TabPanel value="retry" selected={props.selected}>
<RetryTasksTable
queue={props.queue}
totalTaskCount={currentStats.retry}
/>
</TabPanel>
<TabPanel value="archived" selected={props.selected}>
<ArchivedTasksTable
queue={props.queue}
totalTaskCount={currentStats.archived}
/>
</TabPanel>
<TabPanel value="completed" selected={props.selected}>
<CompletedTasksTable
queue={props.queue}
totalTaskCount={currentStats.completed}
/>
</TabPanel>
</Paper>
);
export interface RowProps {
key: string;
task: TaskInfoExtended;
isSelected: boolean;
onSelectChange: (checked: boolean) => void;
onRunClick?: () => void;
onDeleteClick?: () => void;
onArchiveClick?: () => void;
onCancelClick?: () => void;
allActionPending: boolean;
showActions: boolean;
onActionCellEnter: () => void;
onActionCellLeave: () => void;
}
export default connector(TasksTable);

View File

@ -0,0 +1,262 @@
import React, { useState } from "react";
import { connect, ConnectedProps } from "react-redux";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import Paper from "@material-ui/core/Paper";
import Chip from "@material-ui/core/Chip";
import InputBase from "@material-ui/core/InputBase";
import SearchIcon from "@material-ui/icons/Search";
import ActiveTasksTable from "./ActiveTasksTable";
import PendingTasksTable from "./PendingTasksTable";
import ScheduledTasksTable from "./ScheduledTasksTable";
import RetryTasksTable from "./RetryTasksTable";
import ArchivedTasksTable from "./ArchivedTasksTable";
import CompletedTasksTable from "./CompletedTasksTable";
import AggregatingTasksTableContainer from "./AggregatingTasksTableContainer";
import { useHistory } from "react-router-dom";
import { queueDetailsPath, taskDetailsPath } from "../paths";
import { QueueInfo } from "../reducers/queuesReducer";
import { AppState } from "../store";
import { isDarkTheme } from "../theme";
interface TabPanelProps {
children?: React.ReactNode;
selected: string; // currently selected value
value: string; // tab panel will be shown if selected value equals to the value
}
function TabPanel(props: TabPanelProps) {
const { children, value, selected, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== selected}
id={`scrollable-auto-tabpanel-${selected}`}
aria-labelledby={`scrollable-auto-tab-${selected}`}
style={{ flex: 1, overflowY: "scroll" }}
{...other}
>
{value === selected && children}
</div>
);
}
function mapStatetoProps(state: AppState, ownProps: Props) {
// TODO: Add loading state for each queue.
const queueInfo = state.queues.data.find(
(q: QueueInfo) => q.name === ownProps.queue
);
const currentStats = queueInfo
? queueInfo.currentStats
: {
queue: ownProps.queue,
paused: false,
size: 0,
groups: 0,
active: 0,
pending: 0,
aggregating: 0,
scheduled: 0,
retry: 0,
archived: 0,
completed: 0,
processed: 0,
failed: 0,
timestamp: "n/a",
};
return { currentStats };
}
const connector = connect(mapStatetoProps);
type ReduxProps = ConnectedProps<typeof connector>;
interface Props {
queue: string;
selected: string;
}
const useStyles = makeStyles((theme) => ({
container: {
width: "100%",
height: "100%",
background: theme.palette.background.paper,
},
header: {
display: "flex",
alignItems: "center",
paddingTop: theme.spacing(1),
},
heading: {
paddingTop: theme.spacing(1),
paddingBottom: theme.spacing(1),
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
},
chip: {
marginLeft: theme.spacing(1),
},
taskcount: {
fontSize: "12px",
color: theme.palette.text.secondary,
background: isDarkTheme(theme)
? "#303030"
: theme.palette.background.default,
textAlign: "center",
padding: "3px 6px",
borderRadius: "10px",
marginLeft: "2px",
},
searchbar: {
paddingLeft: theme.spacing(1),
paddingRight: theme.spacing(1),
marginRight: theme.spacing(1),
flex: 1,
},
search: {
position: "relative",
maxWidth: 400,
borderRadius: "18px",
backgroundColor: isDarkTheme(theme) ? "#303030" : theme.palette.grey[100],
"&:hover, &:focus": {
backgroundColor: isDarkTheme(theme) ? "#303030" : theme.palette.grey[200],
},
},
searchIcon: {
padding: theme.spacing(0, 2),
height: "100%",
position: "absolute",
pointerEvents: "none",
display: "flex",
alignItems: "center",
justifyContent: "center",
},
inputRoot: {
color: "inherit",
width: "100%",
},
inputInput: {
padding: theme.spacing(1, 1, 1, 0),
// vertical padding + font size from searchIcon
paddingLeft: `calc(1em + ${theme.spacing(4)}px)`,
width: "100%",
fontSize: "0.85rem",
},
}));
function TasksTableContainer(props: Props & ReduxProps) {
const { currentStats } = props;
const classes = useStyles();
const history = useHistory();
const chips = [
{ key: "active", label: "Active", count: currentStats.active },
{ key: "pending", label: "Pending", count: currentStats.pending },
{
key: "aggregating",
label: "Aggregating",
count: currentStats.aggregating,
},
{ 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>("");
return (
<Paper variant="outlined" className={classes.container}>
<div className={classes.header}>
<Typography color="textPrimary" className={classes.heading}>
Tasks
</Typography>
<div>
{chips.map((c) => (
<Chip
key={c.key}
className={classes.chip}
label={
<div>
{c.label} <span className={classes.taskcount}>{c.count}</span>
</div>
}
variant="outlined"
color={props.selected === c.key ? "primary" : "default"}
onClick={() => history.push(queueDetailsPath(props.queue, c.key))}
/>
))}
</div>
<div className={classes.searchbar}>
<div className={classes.search}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
<InputBase
placeholder="Search by ID"
classes={{
root: classes.inputRoot,
input: classes.inputInput,
}}
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
}}
inputProps={{
"aria-label": "search",
onKeyDown: (e) => {
if (e.key === "Enter") {
history.push(
taskDetailsPath(props.queue, searchQuery.trim())
);
}
},
}}
/>
</div>
</div>
</div>
<TabPanel value="active" selected={props.selected}>
<ActiveTasksTable
queue={props.queue}
totalTaskCount={currentStats.active}
/>
</TabPanel>
<TabPanel value="pending" selected={props.selected}>
<PendingTasksTable
queue={props.queue}
totalTaskCount={currentStats.pending}
/>
</TabPanel>
<TabPanel value="aggregating" selected={props.selected}>
<AggregatingTasksTableContainer queue={props.queue} />
</TabPanel>
<TabPanel value="scheduled" selected={props.selected}>
<ScheduledTasksTable
queue={props.queue}
totalTaskCount={currentStats.scheduled}
/>
</TabPanel>
<TabPanel value="retry" selected={props.selected}>
<RetryTasksTable
queue={props.queue}
totalTaskCount={currentStats.retry}
/>
</TabPanel>
<TabPanel value="archived" selected={props.selected}>
<ArchivedTasksTable
queue={props.queue}
totalTaskCount={currentStats.archived}
/>
</TabPanel>
<TabPanel value="completed" selected={props.selected}>
<CompletedTasksTable
queue={props.queue}
totalTaskCount={currentStats.completed}
/>
</TabPanel>
</Paper>
);
}
export default connector(TasksTableContainer);

9
ui/src/global.d.ts vendored
View File

@ -1,4 +1,10 @@
interface Window {
// FLAG values are assigned by server under the window object.
// parseFlagsUnderWindow function parses these values and assigns the interpretted value under the window.
FLAG_ROOT_PATH: string;
FLAG_PROMETHEUS_SERVER_ADDRESS: string;
FLAG_READ_ONLY: string;
// Root URL path for asynqmon app.
// ROOT_PATH should not have the tailing slash.
ROOT_PATH: string;
@ -6,4 +12,7 @@ interface Window {
// Prometheus server address to query time series data.
// This field is set to empty string by default. Use this field only if it's set.
PROMETHEUS_SERVER_ADDRESS: string;
// If true, app hides buttons/links to make non-GET requests to the API server.
READ_ONLY: boolean;
}

View File

@ -4,10 +4,13 @@ import CssBaseline from "@material-ui/core/CssBaseline";
import { Provider } from "react-redux";
import App from "./App";
import store from "./store";
import parseFlagsUnderWindow from "./parseFlags";
import * as serviceWorker from "./serviceWorker";
import { saveState } from "./localStorage";
import { SettingsState } from "./reducers/settingsReducer";
parseFlagsUnderWindow();
let currentSettings: SettingsState | undefined = undefined;
store.subscribe(() => {
const prevSettings = currentSettings;

43
ui/src/parseFlags.ts Normal file
View File

@ -0,0 +1,43 @@
// Prefix used for go template
const goTmplActionPrefix = "/[[";
// paseses flags (string values) assigned under the window objects by server.
export default function parseFlagsUnderWindow() {
// ROOT_PATH
if (window.FLAG_ROOT_PATH === undefined) {
console.log("ROOT_PATH is not defined. Falling back to emtpy string");
window.ROOT_PATH = "";
} else {
window.ROOT_PATH = window.FLAG_ROOT_PATH;
}
// PROMETHEUS_SERVER_ADDRESS
if (window.FLAG_PROMETHEUS_SERVER_ADDRESS === undefined) {
console.log(
"PROMETHEUS_SERVER_ADDRESS is not defined. Falling back to emtpy string"
);
window.PROMETHEUS_SERVER_ADDRESS = "";
} else if (
window.FLAG_PROMETHEUS_SERVER_ADDRESS.startsWith(goTmplActionPrefix)
) {
console.log(
"PROMETHEUS_SERVER_ADDRESS was not evaluated by the server. Falling back to empty string"
);
window.PROMETHEUS_SERVER_ADDRESS = "";
} else {
window.PROMETHEUS_SERVER_ADDRESS = window.FLAG_PROMETHEUS_SERVER_ADDRESS;
}
// READ_ONLY
if (window.FLAG_READ_ONLY === undefined) {
console.log("READ_ONLY is not defined. Falling back to false");
window.READ_ONLY = false;
} else if (window.FLAG_READ_ONLY.startsWith(goTmplActionPrefix)) {
console.log(
"READ_ONLY was not evaluated by the server. Falling back to false"
);
window.READ_ONLY = false;
} else {
window.READ_ONLY = window.FLAG_READ_ONLY === "true";
}
}

View File

@ -1,4 +1,4 @@
export const paths = {
export const paths = () => ({
HOME: `${window.ROOT_PATH}/`,
SETTINGS: `${window.ROOT_PATH}/settings`,
SERVERS: `${window.ROOT_PATH}/servers`,
@ -7,14 +7,14 @@ export const paths = {
REDIS: `${window.ROOT_PATH}/redis`,
TASK_DETAILS: `${window.ROOT_PATH}/queues/:qname/tasks/:taskId`,
QUEUE_METRICS: `${window.ROOT_PATH}/q/metrics`,
};
});
/**************************************************************
Path Helper functions
**************************************************************/
export function queueDetailsPath(qname: string, taskStatus?: string): string {
const path = paths.QUEUE_DETAILS.replace(":qname", qname);
const path = paths().QUEUE_DETAILS.replace(":qname", qname);
if (taskStatus) {
return `${path}?status=${taskStatus}`;
}
@ -22,7 +22,9 @@ export function queueDetailsPath(qname: string, taskStatus?: string): string {
}
export function taskDetailsPath(qname: string, taskId: string): string {
return paths.TASK_DETAILS.replace(":qname", qname).replace(":taskId", taskId);
return paths()
.TASK_DETAILS.replace(":qname", qname)
.replace(":taskId", taskId);
}
/**************************************************************

View File

@ -0,0 +1,55 @@
import {
GroupsActionTypes,
LIST_GROUPS_BEGIN,
LIST_GROUPS_ERROR,
LIST_GROUPS_SUCCESS,
} from "../actions/groupsActions";
import {
LIST_AGGREGATING_TASKS_SUCCESS,
TasksActionTypes,
} from "../actions/tasksActions";
import { GroupInfo } from "../api";
interface GroupsState {
loading: boolean;
data: GroupInfo[];
error: string;
}
const initialState: GroupsState = {
data: [],
loading: false,
error: "",
};
function groupsReducer(
state = initialState,
action: GroupsActionTypes | TasksActionTypes
): GroupsState {
switch (action.type) {
case LIST_GROUPS_BEGIN:
return { ...state, loading: true };
case LIST_GROUPS_ERROR:
return { ...state, loading: false, error: action.error };
case LIST_GROUPS_SUCCESS:
return {
...state,
loading: false,
error: "",
data: action.payload.groups,
};
case LIST_AGGREGATING_TASKS_SUCCESS:
return {
...state,
data: action.payload.groups,
};
default:
return state;
}
}
export default groupsReducer;

View File

@ -1,3 +1,7 @@
import {
GroupsActionTypes,
LIST_GROUPS_SUCCESS,
} from "../actions/groupsActions";
import {
LIST_QUEUES_SUCCESS,
LIST_QUEUES_BEGIN,
@ -53,6 +57,16 @@ import {
DELETE_COMPLETED_TASK_SUCCESS,
DELETE_ALL_COMPLETED_TASKS_SUCCESS,
BATCH_DELETE_COMPLETED_TASKS_SUCCESS,
DELETE_ALL_AGGREGATING_TASKS_SUCCESS,
ARCHIVE_ALL_AGGREGATING_TASKS_SUCCESS,
RUN_ALL_AGGREGATING_TASKS_SUCCESS,
BATCH_DELETE_AGGREGATING_TASKS_SUCCESS,
BATCH_RUN_AGGREGATING_TASKS_SUCCESS,
BATCH_ARCHIVE_AGGREGATING_TASKS_SUCCESS,
DELETE_AGGREGATING_TASK_SUCCESS,
RUN_AGGREGATING_TASK_SUCCESS,
ARCHIVE_AGGREGATING_TASK_SUCCESS,
LIST_AGGREGATING_TASKS_SUCCESS,
} from "../actions/tasksActions";
import { Queue } from "../api";
@ -72,7 +86,7 @@ const initialState: QueuesState = { data: [], loading: false, error: "" };
function queuesReducer(
state = initialState,
action: QueuesActionTypes | TasksActionTypes
action: QueuesActionTypes | TasksActionTypes | GroupsActionTypes
): QueuesState {
switch (action.type) {
case LIST_QUEUES_BEGIN:
@ -162,6 +176,7 @@ function queuesReducer(
case LIST_ACTIVE_TASKS_SUCCESS:
case LIST_PENDING_TASKS_SUCCESS:
case LIST_AGGREGATING_TASKS_SUCCESS:
case LIST_SCHEDULED_TASKS_SUCCESS:
case LIST_RETRY_TASKS_SUCCESS:
case LIST_ARCHIVED_TASKS_SUCCESS: {
@ -175,6 +190,23 @@ function queuesReducer(
return { ...state, data: newData };
}
case RUN_AGGREGATING_TASK_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
return queueInfo;
}
return {
...queueInfo,
currentStats: {
...queueInfo.currentStats,
pending: queueInfo.currentStats.pending + 1,
aggregating: queueInfo.currentStats.aggregating - 1,
},
};
});
return { ...state, data: newData };
}
case RUN_SCHEDULED_TASK_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
@ -243,6 +275,23 @@ function queuesReducer(
return { ...state, data: newData };
}
case ARCHIVE_AGGREGATING_TASK_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
return queueInfo;
}
return {
...queueInfo,
currentStats: {
...queueInfo.currentStats,
archived: queueInfo.currentStats.archived + 1,
aggregating: queueInfo.currentStats.aggregating - 1,
},
};
});
return { ...state, data: newData };
}
case ARCHIVE_SCHEDULED_TASK_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
@ -311,6 +360,23 @@ function queuesReducer(
return { ...state, data: newData };
}
case DELETE_AGGREGATING_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,
aggregating: queueInfo.currentStats.aggregating - 1,
},
};
});
return { ...state, data: newData };
}
case BATCH_ARCHIVE_PENDING_TASKS_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
@ -370,6 +436,24 @@ function queuesReducer(
return { ...state, data: newData };
}
case ARCHIVE_ALL_AGGREGATING_TASKS_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
return queueInfo;
}
return {
...queueInfo,
currentStats: {
...queueInfo.currentStats,
groups: queueInfo.currentStats.groups - 1,
archived: queueInfo.currentStats.archived + action.archived,
aggregating: queueInfo.currentStats.aggregating - action.archived,
},
};
});
return { ...state, data: newData };
}
case DELETE_ALL_PENDING_TASKS_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
@ -449,6 +533,24 @@ function queuesReducer(
return { ...state, data: newData };
}
case RUN_ALL_AGGREGATING_TASKS_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
return queueInfo;
}
return {
...queueInfo,
currentStats: {
...queueInfo.currentStats,
groups: queueInfo.currentStats.groups - 1,
pending: queueInfo.currentStats.pending + action.scheduled,
aggregating: queueInfo.currentStats.aggregating - action.scheduled,
},
};
});
return { ...state, data: newData };
}
case RUN_ALL_SCHEDULED_TASKS_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
@ -486,6 +588,24 @@ function queuesReducer(
return { ...state, data: newData };
}
case DELETE_ALL_AGGREGATING_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,
groups: queueInfo.currentStats.groups - 1,
aggregating: queueInfo.currentStats.aggregating - action.deleted,
},
};
});
return { ...state, data: newData };
}
case DELETE_ALL_SCHEDULED_TASKS_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
@ -579,6 +699,68 @@ function queuesReducer(
return { ...state, data: newData };
}
case BATCH_RUN_AGGREGATING_TASKS_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
return queueInfo;
}
return {
...queueInfo,
currentStats: {
...queueInfo.currentStats,
pending:
queueInfo.currentStats.pending +
action.payload.pending_ids.length,
aggregating:
queueInfo.currentStats.aggregating -
action.payload.pending_ids.length,
},
};
});
return { ...state, data: newData };
}
case BATCH_ARCHIVE_AGGREGATING_TASKS_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
return queueInfo;
}
return {
...queueInfo,
currentStats: {
...queueInfo.currentStats,
archived:
queueInfo.currentStats.archived +
action.payload.archived_ids.length,
aggregating:
queueInfo.currentStats.aggregating -
action.payload.archived_ids.length,
},
};
});
return { ...state, data: newData };
}
case BATCH_DELETE_AGGREGATING_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,
aggregating:
queueInfo.currentStats.aggregating -
action.payload.deleted_ids.length,
},
};
});
return { ...state, data: newData };
}
case RUN_ALL_RETRY_TASKS_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
@ -779,6 +961,19 @@ function queuesReducer(
return { ...state, data: newData };
}
case LIST_GROUPS_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
return queueInfo;
}
return {
...queueInfo,
currentStats: action.payload.stats,
};
});
return { ...state, data: newData };
}
default:
return state;
}

View File

@ -39,6 +39,15 @@ import {
DELETE_COMPLETED_TASK_SUCCESS,
DELETE_ALL_COMPLETED_TASKS_SUCCESS,
BATCH_DELETE_COMPLETED_TASKS_SUCCESS,
RUN_AGGREGATING_TASK_SUCCESS,
ARCHIVE_AGGREGATING_TASK_SUCCESS,
DELETE_AGGREGATING_TASK_SUCCESS,
BATCH_RUN_AGGREGATING_TASKS_SUCCESS,
BATCH_ARCHIVE_AGGREGATING_TASKS_SUCCESS,
BATCH_DELETE_AGGREGATING_TASKS_SUCCESS,
RUN_ALL_AGGREGATING_TASKS_SUCCESS,
ARCHIVE_ALL_AGGREGATING_TASKS_SUCCESS,
DELETE_ALL_AGGREGATING_TASKS_SUCCESS,
} from "../actions/tasksActions";
interface SnackbarState {
@ -98,6 +107,12 @@ function snackbarReducer(
message: `Archived task is now pending`,
};
case RUN_AGGREGATING_TASK_SUCCESS:
return {
isOpen: true,
message: `Aggregating task is now pending`,
};
case ARCHIVE_PENDING_TASK_SUCCESS:
return {
isOpen: true,
@ -116,6 +131,12 @@ function snackbarReducer(
message: `Retry task is now archived`,
};
case ARCHIVE_AGGREGATING_TASK_SUCCESS:
return {
isOpen: true,
message: `Aggregating task is now archived`,
};
case DELETE_PENDING_TASK_SUCCESS:
return {
isOpen: true,
@ -128,6 +149,12 @@ function snackbarReducer(
message: `Scheduled task deleted`,
};
case DELETE_AGGREGATING_TASK_SUCCESS:
return {
isOpen: true,
message: `Aggregating task deleted`,
};
case BATCH_RUN_SCHEDULED_TASKS_SUCCESS: {
const n = action.payload.pending_ids.length;
return {
@ -138,6 +165,34 @@ function snackbarReducer(
};
}
case BATCH_RUN_AGGREGATING_TASKS_SUCCESS: {
const n = action.payload.pending_ids.length;
return {
isOpen: true,
message: `${n} aggregating ${
n === 1 ? "task is" : "tasks are"
} now pending`,
};
}
case BATCH_ARCHIVE_AGGREGATING_TASKS_SUCCESS: {
const n = action.payload.archived_ids.length;
return {
isOpen: true,
message: `${n} aggregating ${
n === 1 ? "task is" : "tasks are"
} now archived`,
};
}
case BATCH_DELETE_AGGREGATING_TASKS_SUCCESS: {
const n = action.payload.deleted_ids.length;
return {
isOpen: true,
message: `${n} aggregating ${n === 1 ? "task" : "tasks"} deleted`,
};
}
case BATCH_ARCHIVE_PENDING_TASKS_SUCCESS: {
const n = action.payload.archived_ids.length;
return {
@ -186,6 +241,24 @@ function snackbarReducer(
message: "All pending tasks deleted",
};
case RUN_ALL_AGGREGATING_TASKS_SUCCESS:
return {
isOpen: true,
message: `All tasks in group ${action.group} are now pending`,
};
case ARCHIVE_ALL_AGGREGATING_TASKS_SUCCESS:
return {
isOpen: true,
message: `All tasks in group ${action.group} are now archived`,
};
case DELETE_ALL_AGGREGATING_TASKS_SUCCESS:
return {
isOpen: true,
message: `All tasks in group ${action.group} deleted`,
};
case RUN_ALL_SCHEDULED_TASKS_SUCCESS:
return {
isOpen: true,

View File

@ -129,23 +129,49 @@ import {
BATCH_DELETE_COMPLETED_TASKS_BEGIN,
BATCH_DELETE_COMPLETED_TASKS_ERROR,
BATCH_DELETE_COMPLETED_TASKS_SUCCESS,
LIST_AGGREGATING_TASKS_BEGIN,
LIST_AGGREGATING_TASKS_SUCCESS,
LIST_AGGREGATING_TASKS_ERROR,
DELETE_ALL_AGGREGATING_TASKS_BEGIN,
DELETE_ALL_AGGREGATING_TASKS_SUCCESS,
DELETE_ALL_AGGREGATING_TASKS_ERROR,
ARCHIVE_ALL_AGGREGATING_TASKS_BEGIN,
ARCHIVE_ALL_AGGREGATING_TASKS_SUCCESS,
ARCHIVE_ALL_AGGREGATING_TASKS_ERROR,
RUN_ALL_AGGREGATING_TASKS_BEGIN,
RUN_ALL_AGGREGATING_TASKS_SUCCESS,
RUN_ALL_AGGREGATING_TASKS_ERROR,
BATCH_ARCHIVE_AGGREGATING_TASKS_BEGIN,
BATCH_RUN_AGGREGATING_TASKS_BEGIN,
BATCH_DELETE_AGGREGATING_TASKS_BEGIN,
BATCH_RUN_AGGREGATING_TASKS_ERROR,
BATCH_ARCHIVE_AGGREGATING_TASKS_ERROR,
BATCH_DELETE_AGGREGATING_TASKS_ERROR,
BATCH_DELETE_AGGREGATING_TASKS_SUCCESS,
BATCH_RUN_AGGREGATING_TASKS_SUCCESS,
BATCH_ARCHIVE_AGGREGATING_TASKS_SUCCESS,
RUN_AGGREGATING_TASK_BEGIN,
ARCHIVE_AGGREGATING_TASK_BEGIN,
DELETE_AGGREGATING_TASK_BEGIN,
RUN_AGGREGATING_TASK_ERROR,
ARCHIVE_AGGREGATING_TASK_ERROR,
DELETE_AGGREGATING_TASK_ERROR,
RUN_AGGREGATING_TASK_SUCCESS,
ARCHIVE_AGGREGATING_TASK_SUCCESS,
DELETE_AGGREGATING_TASK_SUCCESS,
} from "../actions/tasksActions";
import { TaskInfo } from "../api";
export interface ActiveTaskExtended extends TaskInfo {
export interface TaskInfoExtended extends TaskInfo {
// Indicates that a request has been sent for this
// task and awaiting for a response.
requestPending: boolean;
// Incidates that a cancelation signal has been
// published for this task.
canceling: boolean;
}
export interface TaskInfoExtended extends TaskInfo {
// Indicates that a request has been sent for this
// task and awaiting for a response.
requestPending: boolean;
//
// Only applies to active tasks
canceling?: boolean;
}
interface TasksState {
@ -154,7 +180,7 @@ interface TasksState {
batchActionPending: boolean;
allActionPending: boolean;
error: string;
data: ActiveTaskExtended[];
data: TaskInfoExtended[];
};
pendingTasks: {
loading: boolean;
@ -191,6 +217,14 @@ interface TasksState {
error: string;
data: TaskInfoExtended[];
};
aggregatingTasks: {
group: string;
loading: boolean;
batchActionPending: boolean;
allActionPending: boolean;
error: string;
data: TaskInfoExtended[];
};
taskInfo: {
loading: boolean;
error: string;
@ -241,6 +275,14 @@ const initialState: TasksState = {
error: "",
data: [],
},
aggregatingTasks: {
group: "",
loading: false,
batchActionPending: false,
allActionPending: false,
error: "",
data: [],
},
taskInfo: {
loading: false,
error: "",
@ -485,6 +527,43 @@ function tasksReducer(
},
};
case LIST_AGGREGATING_TASKS_BEGIN:
return {
...state,
aggregatingTasks: {
...state.aggregatingTasks,
group: action.group,
loading: true,
},
};
case LIST_AGGREGATING_TASKS_SUCCESS:
return {
...state,
aggregatingTasks: {
...state.aggregatingTasks,
group: action.group,
loading: false,
error: "",
data: action.payload.tasks.map((task) => ({
...task,
requestPending: false,
})),
},
};
case LIST_AGGREGATING_TASKS_ERROR:
return {
...state,
aggregatingTasks: {
...state.aggregatingTasks,
group: action.group,
loading: false,
error: action.error,
data: [],
},
};
case DELETE_COMPLETED_TASK_BEGIN:
return {
...state,
@ -1046,6 +1125,148 @@ function tasksReducer(
},
};
case RUN_AGGREGATING_TASK_BEGIN:
case ARCHIVE_AGGREGATING_TASK_BEGIN:
case DELETE_AGGREGATING_TASK_BEGIN:
return {
...state,
aggregatingTasks: {
...state.aggregatingTasks,
data: state.aggregatingTasks.data.map((task) => {
if (task.id !== action.taskId) {
return task;
}
return { ...task, requestPending: true };
}),
},
};
case RUN_AGGREGATING_TASK_ERROR:
case ARCHIVE_AGGREGATING_TASK_ERROR:
case DELETE_AGGREGATING_TASK_ERROR:
return {
...state,
aggregatingTasks: {
...state.aggregatingTasks,
data: state.aggregatingTasks.data.map((task) => {
if (task.id !== action.taskId) {
return task;
}
return { ...task, requestPending: false };
}),
},
};
case RUN_AGGREGATING_TASK_SUCCESS:
case ARCHIVE_AGGREGATING_TASK_SUCCESS:
case DELETE_AGGREGATING_TASK_SUCCESS:
return {
...state,
aggregatingTasks: {
...state.aggregatingTasks,
data: state.aggregatingTasks.data.filter(
(task) => task.id !== action.taskId
),
},
};
case BATCH_RUN_AGGREGATING_TASKS_BEGIN:
case BATCH_ARCHIVE_AGGREGATING_TASKS_BEGIN:
case BATCH_DELETE_AGGREGATING_TASKS_BEGIN:
if (action.group !== state.aggregatingTasks.group) {
return state;
}
return {
...state,
aggregatingTasks: {
...state.aggregatingTasks,
batchActionPending: true,
data: state.aggregatingTasks.data.map((task) => {
if (!action.taskIds.includes(task.id)) {
return task;
}
return {
...task,
requestPending: true,
};
}),
},
};
case BATCH_RUN_AGGREGATING_TASKS_ERROR:
case BATCH_ARCHIVE_AGGREGATING_TASKS_ERROR:
case BATCH_DELETE_AGGREGATING_TASKS_ERROR:
if (action.group !== state.aggregatingTasks.group) {
return state;
}
return {
...state,
aggregatingTasks: {
...state.aggregatingTasks,
batchActionPending: false,
data: state.scheduledTasks.data.map((task) => {
if (!action.taskIds.includes(task.id)) {
return task;
}
return {
...task,
requestPending: false,
};
}),
},
};
case BATCH_DELETE_AGGREGATING_TASKS_SUCCESS: {
if (action.group !== state.aggregatingTasks.group) {
return state;
}
const newData = state.aggregatingTasks.data.filter(
(task) => !action.payload.deleted_ids.includes(task.id)
);
return {
...state,
aggregatingTasks: {
...state.aggregatingTasks,
batchActionPending: false,
data: newData,
},
};
}
case BATCH_RUN_AGGREGATING_TASKS_SUCCESS: {
if (action.group !== state.aggregatingTasks.group) {
return state;
}
const newData = state.aggregatingTasks.data.filter(
(task) => !action.payload.pending_ids.includes(task.id)
);
return {
...state,
aggregatingTasks: {
...state.aggregatingTasks,
batchActionPending: false,
data: newData,
},
};
}
case BATCH_ARCHIVE_AGGREGATING_TASKS_SUCCESS: {
if (action.group !== state.aggregatingTasks.group) {
return state;
}
const newData = state.aggregatingTasks.data.filter(
(task) => !action.payload.archived_ids.includes(task.id)
);
return {
...state,
aggregatingTasks: {
...state.aggregatingTasks,
batchActionPending: false,
data: newData,
},
};
}
case RUN_RETRY_TASK_BEGIN:
case ARCHIVE_RETRY_TASK_BEGIN:
case DELETE_RETRY_TASK_BEGIN:
@ -1346,6 +1567,50 @@ function tasksReducer(
},
};
case RUN_ALL_AGGREGATING_TASKS_BEGIN:
case ARCHIVE_ALL_AGGREGATING_TASKS_BEGIN:
case DELETE_ALL_AGGREGATING_TASKS_BEGIN:
if (state.aggregatingTasks.group !== action.group) {
return state;
}
return {
...state,
aggregatingTasks: {
...state.aggregatingTasks,
allActionPending: true,
},
};
case RUN_ALL_AGGREGATING_TASKS_SUCCESS:
case ARCHIVE_ALL_AGGREGATING_TASKS_SUCCESS:
case DELETE_ALL_AGGREGATING_TASKS_SUCCESS:
if (state.aggregatingTasks.group !== action.group) {
return state;
}
return {
...state,
aggregatingTasks: {
...state.aggregatingTasks,
allActionPending: false,
data: [],
},
};
case RUN_ALL_AGGREGATING_TASKS_ERROR:
case ARCHIVE_ALL_AGGREGATING_TASKS_ERROR:
case DELETE_ALL_AGGREGATING_TASKS_ERROR:
if (state.aggregatingTasks.group !== action.group) {
return state;
}
return {
...state,
aggregatingTasks: {
...state.aggregatingTasks,
allActionPending: false,
error: action.error,
},
};
default:
return state;
}

View File

@ -2,6 +2,7 @@ import { combineReducers, configureStore } from "@reduxjs/toolkit";
import settingsReducer from "./reducers/settingsReducer";
import queuesReducer from "./reducers/queuesReducer";
import tasksReducer from "./reducers/tasksReducer";
import groupsReducer from "./reducers/groupsReducer";
import serversReducer from "./reducers/serversReducer";
import schedulerEntriesReducer from "./reducers/schedulerEntriesReducer";
import snackbarReducer from "./reducers/snackbarReducer";
@ -14,6 +15,7 @@ const rootReducer = combineReducers({
settings: settingsReducer,
queues: queuesReducer,
tasks: tasksReducer,
groups: groupsReducer,
servers: serversReducer,
schedulerEntries: schedulerEntriesReducer,
snackbar: snackbarReducer,

View File

@ -0,0 +1,8 @@
export type TaskState =
| "active"
| "pending"
| "aggregating"
| "scheduled"
| "retry"
| "archived"
| "completed";

View File

@ -64,7 +64,11 @@ export function durationBefore(timestamp: string): string {
}
}
const zeroTimestamp = "0001-01-01T00:00:00Z";
export function timeAgo(timestamp: string): string {
if (timestamp === zeroTimestamp) {
return "-";
}
try {
return timeAgoUnix(Date.parse(timestamp) / 1000);
} catch (error) {

View File

@ -3,7 +3,7 @@ import { connect, ConnectedProps } from "react-redux";
import { makeStyles } from "@material-ui/core/styles";
import Container from "@material-ui/core/Container";
import Grid from "@material-ui/core/Grid";
import TasksTable from "../components/TasksTable";
import TasksTableContainer from "../components/TasksTableContainer";
import QueueInfoBanner from "../components/QueueInfoBanner";
import QueueBreadCrumb from "../components/QueueBreadcrumb";
import { useParams } from "react-router-dom";
@ -38,6 +38,7 @@ const useStyles = makeStyles((theme) => ({
const validStatus = [
"active",
"pending",
"aggregating",
"scheduled",
"retry",
"archived",
@ -69,7 +70,7 @@ function TasksView(props: ConnectedProps<typeof connector>) {
<QueueInfoBanner qname={qname} />
</Grid>
<Grid item xs={12} className={classes.tasksTable}>
<TasksTable queue={qname} selected={selected} />
<TasksTableContainer queue={qname} selected={selected} />
</Grid>
</Grid>
</Container>

View File

@ -1111,6 +1111,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.0.0":
version "7.17.8"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2"
integrity sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
version "7.15.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a"
@ -2029,6 +2036,13 @@
dependencies:
"@types/react" "*"
"@types/react-window@1.8.5":
version "1.8.5"
resolved "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1"
integrity sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^17.0.29":
version "17.0.29"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.29.tgz#9535f3fc01a4981ce9cadcf0daa2593c0c2f2251"
@ -7740,6 +7754,11 @@ media-typer@0.3.0:
resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
"memoize-one@>=3.1.1 <6":
version "5.2.1"
resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
memory-fs@^0.4.1:
version "0.4.1"
resolved "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
@ -9931,6 +9950,14 @@ react-transition-group@^4.4.0:
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-window@1.8.6:
version "1.8.6"
resolved "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz#d011950ac643a994118632665aad0c6382e2a112"
integrity sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg==
dependencies:
"@babel/runtime" "^7.0.0"
memoize-one ">=3.1.1 <6"
react@^16.13.1:
version "16.14.0"
resolved "https://registry.npmjs.org/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"