mirror of
https://github.com/hibiken/asynqmon.git
synced 2025-04-21 00:00:12 +08:00
Compare commits
111 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d1b889456d | ||
|
e44ba437a4 | ||
|
5c48e4e31d | ||
|
2cb4c8c1bc | ||
|
3f4e7615fb | ||
|
bda90ac732 | ||
|
1597dac66e | ||
|
b3b8c2d13d | ||
|
b7c2ebeff3 | ||
|
0527b6c483 | ||
|
6dbc580738 | ||
|
2f9d2021c3 | ||
|
9796da746b | ||
|
9ef529e8c5 | ||
|
8a73386bd7 | ||
|
6fbf82f3e2 | ||
|
7e0ae2b4a6 | ||
|
c22c0206d7 | ||
|
726d58fcda | ||
|
6387d9cc67 | ||
|
97d969171f | ||
|
cd6947ef20 | ||
|
bb26dda300 | ||
|
d0a8b6b691 | ||
|
9de7f054bc | ||
|
0a6150e935 | ||
|
5d9e4aec9c | ||
|
c8d7da05eb | ||
|
33e76f263d | ||
|
ad20a8a7e7 | ||
|
b9254e8c65 | ||
|
c139200b10 | ||
|
28b1d463d0 | ||
|
ff63a289a2 | ||
|
ad687c4dc7 | ||
|
c13cba0d5d | ||
|
db8b77591e | ||
|
a479098bd6 | ||
|
f6d84b1dc2 | ||
|
e4b7765277 | ||
|
55de4e84eb | ||
|
f31f248937 | ||
|
99f147df66 | ||
|
3cd2cbe6f2 | ||
|
b7667d8c7b | ||
|
81eed7e33d | ||
|
33b24ca940 | ||
|
a2b6925041 | ||
|
568e2e301c | ||
|
a0e80ca4da | ||
|
6ec87cd434 | ||
|
56976997d2 | ||
|
d31a42d85d | ||
|
fc7b4a10bf | ||
|
3e9365882d | ||
|
14effdde06 | ||
|
1601a0861a | ||
|
fe6898e75e | ||
|
af9c47d038 | ||
|
834a759680 | ||
|
1a27aaacbe | ||
|
2991ea5a60 | ||
|
15e1eaaa56 | ||
|
49eece97f7 | ||
|
3805ae6e06 | ||
|
c04e63d3f7 | ||
|
ade2baceaf | ||
|
8a508c58eb | ||
|
0d58ef86f4 | ||
|
aceac82d78 | ||
|
1655bf3d88 | ||
|
91683248d0 | ||
|
3d31c94258 | ||
|
747d10df97 | ||
|
609b319a9e | ||
|
3ae79d85c3 | ||
|
e98f285767 | ||
|
e74815a7c1 | ||
|
68e1ce4ee7 | ||
|
5d1bd3cb55 | ||
|
d448ad2525 | ||
|
1b8d46a35e | ||
|
711ca8b5c8 | ||
|
980cdedcc4 | ||
|
d1f7d5dcf4 | ||
|
741a3c59fa | ||
|
ddb1798ce8 | ||
|
bb125b08d6 | ||
|
22599a9bdc | ||
|
32c5bac6cc | ||
|
a40bb37750 | ||
|
b92ef4c369 | ||
|
700a8a7ac6 | ||
|
33ff40b963 | ||
|
917a26def8 | ||
|
e5db8a6299 | ||
|
de23388cf3 | ||
|
fcec027a15 | ||
|
7b49d4e5e2 | ||
|
2f43cf8c1a | ||
|
2116015841 | ||
|
6f2d934c8a | ||
|
08bb249fc0 | ||
|
fbb28b780f | ||
|
09e682842e | ||
|
c3fb3d53a7 | ||
|
ddcdefe60e | ||
|
07e7a5a496 | ||
|
ea5f4192d0 | ||
|
2cd2b67945 | ||
|
18a19526d5 |
62
.github/workflows/codeql-analysis.yml
vendored
62
.github/workflows/codeql-analysis.yml
vendored
@ -2,12 +2,16 @@ name: "CodeQL"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [master]
|
||||||
|
paths-ignore:
|
||||||
|
- ui/build
|
||||||
pull_request:
|
pull_request:
|
||||||
# The branches below must be a subset of the branches above
|
# The branches below must be a subset of the branches above
|
||||||
branches: [ master ]
|
branches: [master]
|
||||||
|
paths-ignore:
|
||||||
|
- ui/build
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '24 0 * * 6'
|
- cron: "24 0 * * 6"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
@ -17,40 +21,40 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
language: [ 'go', 'javascript' ]
|
language: ["go", "javascript"]
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||||
# Learn more:
|
# Learn more:
|
||||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v1
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
# By default, queries listed here will override any specified in a config file.
|
# By default, queries listed here will override any specified in a config file.
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
# Prefix the list here with '+' to use these queries and those in the config file.
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v1
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
# and modify them (or add more) to build your code if your project
|
# and modify them (or add more) to build your code if your project
|
||||||
# uses a compiled language
|
# uses a compiled language
|
||||||
|
|
||||||
#- run: |
|
#- run: |
|
||||||
# make bootstrap
|
# make bootstrap
|
||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@v1
|
||||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -23,8 +23,10 @@ package-json.lock
|
|||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
# main binary
|
# binaries
|
||||||
asynqmon
|
/cmd/asynqmon/asynqmon
|
||||||
|
/asynqmon
|
||||||
|
/api
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
# Editor configs
|
# Editor configs
|
||||||
|
59
CHANGELOG.md
59
CHANGELOG.md
@ -5,6 +5,65 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on ["Keep a Changelog"](https://keepachangelog.com/en/1.0.0/),
|
The format is based on ["Keep a Changelog"](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [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
|
||||||
|
- Updated to be compatible with asynq v0.19
|
||||||
|
|
||||||
|
## [0.3.2] - 2021-10-22
|
||||||
|
|
||||||
|
- (ui): Fixed build
|
||||||
|
|
||||||
|
## [0.3.1] - 2021-10-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- (cmd): Added --max-payload-length to allow specifying number of characters displayed for payload, defaults to 200 chars
|
||||||
|
- (pkg): DefaultPayloadFormatter is now exported from the package
|
||||||
|
|
||||||
|
## [0.3.0]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Asynqmon is now a go package that can be imported to other projects!
|
||||||
|
|
||||||
## [0.2.1]
|
## [0.2.1]
|
||||||
|
|
||||||
### Addded
|
### Addded
|
||||||
|
@ -3,13 +3,15 @@
|
|||||||
# Building a frontend.
|
# Building a frontend.
|
||||||
#
|
#
|
||||||
|
|
||||||
FROM alpine:3.13 AS frontend
|
FROM alpine:3.17 AS frontend
|
||||||
|
|
||||||
# Move to a working directory (/static).
|
# Move to a working directory (/static).
|
||||||
WORKDIR /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).
|
# 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
|
npm i -g -s --unsafe-perm yarn
|
||||||
|
|
||||||
# Copy only ./ui folder to the working directory.
|
# Copy only ./ui folder to the working directory.
|
||||||
@ -23,7 +25,7 @@ RUN yarn install && yarn build
|
|||||||
# Building a backend.
|
# Building a backend.
|
||||||
#
|
#
|
||||||
|
|
||||||
FROM golang:1.16-alpine AS backend
|
FROM golang:1.18-alpine AS backend
|
||||||
|
|
||||||
# Move to a working directory (/build).
|
# Move to a working directory (/build).
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
14
Makefile
14
Makefile
@ -1,7 +1,17 @@
|
|||||||
.PHONY: build docker
|
.PHONY: api assets build docker
|
||||||
|
|
||||||
|
NODE_PATH ?= $(PWD)/ui/node_modules
|
||||||
|
assets:
|
||||||
|
@if [ ! -d "$(NODE_PATH)" ]; then cd ./ui && yarn install --modules-folder $(NODE_PATH); fi
|
||||||
|
cd ./ui && yarn build --modules-folder $(NODE_PATH)
|
||||||
|
|
||||||
|
# This target skips the overhead of building UI assets.
|
||||||
|
# Intended to be used during development.
|
||||||
|
api:
|
||||||
|
go build -o api ./cmd/asynqmon
|
||||||
|
|
||||||
# Build a release binary.
|
# Build a release binary.
|
||||||
build:
|
build: assets
|
||||||
go build -o asynqmon ./cmd/asynqmon
|
go build -o asynqmon ./cmd/asynqmon
|
||||||
|
|
||||||
# Build image and run Asynqmon server (with default settings).
|
# Build image and run Asynqmon server (with default settings).
|
||||||
|
132
README.md
132
README.md
@ -1,20 +1,36 @@
|
|||||||
<img src="https://user-images.githubusercontent.com/11155743/114745460-57760500-9d57-11eb-9a2c-43fa88171807.png" alt="Asynqmon logo" width="360px" />
|
<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 & administrating [Asynq](https://github.com/hibiken/asynq) queues, tasks and message broker
|
# Web UI for monitoring & administering [Asynq](https://github.com/hibiken/asynq) task queue
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
|
Asynqmon is a web UI tool for monitoring and administering [Asynq](https://github.com/hibiken/asynq) queues and tasks.
|
||||||
|
It supports integration with [Prometheus](https://prometheus.io) to display time-series data.
|
||||||
|
|
||||||
Asynqmon is both a library that you can include in your web application, as well as a binary that you can simply install and run.
|
Asynqmon is both a library that you can include in your web application, as well as a binary that you can simply install and run.
|
||||||
|
|
||||||
## Version Compatibility
|
## Version Compatibility
|
||||||
|
|
||||||
|
Please make sure the version compatibility with the Asynq package you are using.
|
||||||
|
|
||||||
| Asynq version | WebUI (asynqmon) version |
|
| Asynq version | WebUI (asynqmon) version |
|
||||||
| -------------- | ------------------------ |
|
| -------------- | ------------------------ |
|
||||||
| 0.18.x | 0.2.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 |
|
| 0.16.x, 0.17.x | 0.1.x |
|
||||||
|
|
||||||
## Install the binary
|
## Install the binary
|
||||||
|
|
||||||
|
There're a few options to install the binary:
|
||||||
|
|
||||||
|
- [Download a release binary](#release-binaries)
|
||||||
|
- [Download a docker image](#docker-image)
|
||||||
|
- [Build a binary from source](building-from-source)
|
||||||
|
- [Build a docker image from source](#building-docker-image-locally)
|
||||||
|
|
||||||
### Release binaries
|
### Release binaries
|
||||||
|
|
||||||
You can download the release binary for your system from the [releases page](https://github.com/hibiken/asynqmon/releases).
|
You can download the release binary for your system from the [releases page](https://github.com/hibiken/asynqmon/releases).
|
||||||
@ -56,10 +72,10 @@ make docker
|
|||||||
To use the defaults, simply run and open http://localhost:8080.
|
To use the defaults, simply run and open http://localhost:8080.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# with a local binary
|
# with a binary
|
||||||
./asynqmon
|
./asynqmon
|
||||||
|
|
||||||
# with docker
|
# with a docker image
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
--name asynqmon \
|
--name asynqmon \
|
||||||
-p 8080:8080 \
|
-p 8080:8080 \
|
||||||
@ -71,10 +87,10 @@ By default, Asynqmon web server listens on port `8080` and connects to a Redis s
|
|||||||
To see all available flags, run:
|
To see all available flags, run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# with a local binary
|
# with a binary
|
||||||
./asynqmon --help
|
./asynqmon --help
|
||||||
|
|
||||||
# with Docker
|
# with a docker image
|
||||||
docker run hibiken/asynqmon --help
|
docker run hibiken/asynqmon --help
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -82,16 +98,59 @@ 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.
|
_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 | Description | Default |
|
| Flag | Env | Description | Default |
|
||||||
| ------------------------------- | ------------------------------------------------------------------- | ---------------- |
|
| --------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------- |
|
||||||
| `--port`(int) | port number to use for web ui server | 8080 |
|
| `--port`(int) | `PORT` | port number to use for web ui server | 8080 |
|
||||||
| `---redis-url`(string) | URL to redis server | "" |
|
| `---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) | address of redis server to connect to | "127.0.0.1:6379" |
|
| `--redis-addr`(string) | `REDIS_ADDR` | address of redis server to connect to | "127.0.0.1:6379" |
|
||||||
| `--redis-db`(int) | redis database number | 0 |
|
| `--redis-db`(int) | `REDIS_DB` | redis database number | 0 |
|
||||||
| `--redis-password`(string) | password to use when connecting to redis server | "" |
|
| `--redis-password`(string) | `REDIS_PASSWORD` | password to use when connecting to redis server | "" |
|
||||||
| `--redis-cluster-nodes`(string) | comma separated list of host:port addresses of cluster nodes | "" |
|
| `--redis-cluster-nodes`(string) | `REDIS_CLUSTER_NODES` | comma separated list of host:port addresses of cluster nodes | "" |
|
||||||
| `--redis-tls`(string) | server name for TLS validation used when connecting to redis server | "" |
|
| `--redis-tls`(string) | `REDIS_TLS` | server name for TLS validation used when connecting to redis server | "" |
|
||||||
| `--redis-insecure-tls`(bool) | disable TLS certificate host checks | false |
|
| `--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
|
||||||
|
|
||||||
|
The binary supports two flags to enable integration with [Prometheus](https://prometheus.io/).
|
||||||
|
|
||||||
|
First, enable metrics exporter to expose queue metrics to Prometheus server by passing `--enable-metrics-exporter` flag.
|
||||||
|
The metrics data is now available under `/metrics` for Prometheus server to scrape.
|
||||||
|
|
||||||
|
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
|
### Examples
|
||||||
|
|
||||||
@ -99,6 +158,9 @@ _Note_: Use `--redis-url` to specify address, db-number, and password with one f
|
|||||||
# with a local binary; custom port and connect to redis server at localhost:6380
|
# with a local binary; custom port and connect to redis server at localhost:6380
|
||||||
./asynqmon --port=3000 --redis-addr=localhost:6380
|
./asynqmon --port=3000 --redis-addr=localhost:6380
|
||||||
|
|
||||||
|
# with prometheus integration enabled
|
||||||
|
./asynqmon --enable-metrics-exporter --prometheus-addr=http://localhost:9090
|
||||||
|
|
||||||
# with Docker (connect to a Redis server running on the host machine)
|
# with Docker (connect to a Redis server running on the host machine)
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
--name asynqmon \
|
--name asynqmon \
|
||||||
@ -125,7 +187,9 @@ Next, go to [localhost:8080](http://localhost:8080) and see Asynqmon dashboard:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Importing into projects
|
## Import as a Library
|
||||||
|
|
||||||
|
[](https://godoc.org/github.com/hibiken/asynqmon)
|
||||||
|
|
||||||
Asynqmon is also a library which can be imported into an existing web application.
|
Asynqmon is also a library which can be imported into an existing web application.
|
||||||
|
|
||||||
@ -148,10 +212,11 @@ func main() {
|
|||||||
RedisConnOpt: asynq.RedisClientOpt{Addr: ":6379"},
|
RedisConnOpt: asynq.RedisClientOpt{Addr: ":6379"},
|
||||||
})
|
})
|
||||||
|
|
||||||
http.Handle(h.RootPath(), h)
|
// Note: We need the tailing slash when using net/http.ServeMux.
|
||||||
|
http.Handle(h.RootPath()+"/", h)
|
||||||
|
|
||||||
// Go to http://localhost:8080/monitoring to see asynqmon homepage.
|
// Go to http://localhost:8080/monitoring to see asynqmon homepage.
|
||||||
log.Fatal(http.ListenAndServe(":8000", nil))
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -188,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
|
## 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).
|
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).
|
||||||
|
@ -1,118 +1,239 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"github.com/rs/cors"
|
|
||||||
|
|
||||||
"github.com/hibiken/asynq"
|
"github.com/hibiken/asynq"
|
||||||
|
"github.com/hibiken/asynq/x/metrics"
|
||||||
"github.com/hibiken/asynqmon"
|
"github.com/hibiken/asynqmon"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
"github.com/rs/cors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Command-line flags
|
// Config holds configurations for the program provided via the command line.
|
||||||
var (
|
type Config struct {
|
||||||
flagPort int
|
// Server port
|
||||||
flagRedisAddr string
|
Port int
|
||||||
flagRedisDB int
|
|
||||||
flagRedisPassword string
|
|
||||||
flagRedisTLS string
|
|
||||||
flagRedisURL string
|
|
||||||
flagRedisInsecureTLS bool
|
|
||||||
flagRedisClusterNodes string
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
// Redis connection options
|
||||||
flag.IntVar(&flagPort, "port", 8080, "port number to use for web ui server")
|
RedisAddr string
|
||||||
flag.StringVar(&flagRedisAddr, "redis-addr", "127.0.0.1:6379", "address of redis server to connect to")
|
RedisDB int
|
||||||
flag.IntVar(&flagRedisDB, "redis-db", 0, "redis database number")
|
RedisPassword string
|
||||||
flag.StringVar(&flagRedisPassword, "redis-password", "", "password to use when connecting to redis server")
|
RedisTLS string
|
||||||
flag.StringVar(&flagRedisTLS, "redis-tls", "", "server name for TLS validation used when connecting to redis server")
|
RedisURL string
|
||||||
flag.StringVar(&flagRedisURL, "redis-url", "", "URL to redis server")
|
RedisInsecureTLS bool
|
||||||
flag.BoolVar(&flagRedisInsecureTLS, "redis-insecure-tls", false, "disable TLS certificate host checks")
|
RedisClusterNodes string
|
||||||
flag.StringVar(&flagRedisClusterNodes, "redis-cluster-nodes", "", "comma separated list of host:port addresses of cluster nodes")
|
|
||||||
|
// 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.
|
// parseFlags parses the command-line arguments provided to the program.
|
||||||
// IDEA: https://eli.thegreenplace.net/2020/testing-flag-parsing-in-go-programs/
|
// Typically, os.Args[0] is provided as 'progname' and os.args[1:] as 'args'.
|
||||||
func getRedisOptionsFromFlags() (asynq.RedisConnOpt, error) {
|
// Returns the Config in case parsing succeeded, or an error. In any case, the
|
||||||
var opts redis.UniversalOptions
|
// 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 != "" {
|
var conf Config
|
||||||
opts.Addrs = strings.Split(flagRedisClusterNodes, ",")
|
flags.IntVar(&conf.Port, "port", getEnvOrDefaultInt("PORT", 8080), "port number to use for web ui server")
|
||||||
opts.Password = flagRedisPassword
|
flags.StringVar(&conf.RedisAddr, "redis-addr", getEnvDefaultString("REDIS_ADDR", "127.0.0.1:6379"), "address of redis server to connect to")
|
||||||
} else {
|
flags.IntVar(&conf.RedisDB, "redis-db", getEnvOrDefaultInt("REDIS_DB", 0), "redis database number")
|
||||||
if flagRedisURL != "" {
|
flags.StringVar(&conf.RedisPassword, "redis-password", getEnvDefaultString("REDIS_PASSWORD", ""), "password to use when connecting to redis server")
|
||||||
res, err := redis.ParseURL(flagRedisURL)
|
flags.StringVar(&conf.RedisTLS, "redis-tls", getEnvDefaultString("REDIS_TLS", ""), "server name for TLS validation used when connecting to redis server")
|
||||||
if err != nil {
|
flags.StringVar(&conf.RedisURL, "redis-url", getEnvDefaultString("REDIS_URL", ""), "URL to redis server")
|
||||||
return nil, err
|
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")
|
||||||
opts.Addrs = append(opts.Addrs, res.Addr)
|
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")
|
||||||
opts.DB = res.DB
|
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")
|
||||||
opts.Password = res.Password
|
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 {
|
err = flags.Parse(args)
|
||||||
opts.Addrs = []string{flagRedisAddr}
|
if err != nil {
|
||||||
opts.DB = flagRedisDB
|
return nil, buf.String(), err
|
||||||
opts.Password = flagRedisPassword
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
conf.Args = flags.Args()
|
||||||
|
return &conf, buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
if flagRedisTLS != "" {
|
func makeTLSConfig(cfg *Config) *tls.Config {
|
||||||
opts.TLSConfig = &tls.Config{ServerName: flagRedisTLS}
|
if cfg.RedisTLS == "" && !cfg.RedisInsecureTLS {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
if flagRedisInsecureTLS {
|
return &tls.Config{
|
||||||
if opts.TLSConfig == nil {
|
ServerName: cfg.RedisTLS,
|
||||||
opts.TLSConfig = &tls.Config{}
|
InsecureSkipVerify: cfg.RedisInsecureTLS,
|
||||||
}
|
|
||||||
opts.TLSConfig.InsecureSkipVerify = true
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if flagRedisClusterNodes != "" {
|
func makeRedisConnOpt(cfg *Config) (asynq.RedisConnOpt, error) {
|
||||||
|
// Connecting to redis-cluster
|
||||||
|
if len(cfg.RedisClusterNodes) > 0 {
|
||||||
return asynq.RedisClusterClientOpt{
|
return asynq.RedisClusterClientOpt{
|
||||||
Addrs: opts.Addrs,
|
Addrs: strings.Split(cfg.RedisClusterNodes, ","),
|
||||||
Password: opts.Password,
|
Password: cfg.RedisPassword,
|
||||||
TLSConfig: opts.TLSConfig,
|
TLSConfig: makeTLSConfig(cfg),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
return asynq.RedisClientOpt{
|
|
||||||
Addr: opts.Addrs[0],
|
// Connecting to redis-sentinels
|
||||||
DB: opts.DB,
|
if strings.HasPrefix(cfg.RedisURL, "redis-sentinel") {
|
||||||
Password: opts.Password,
|
res, err := asynq.ParseRedisURI(cfg.RedisURL)
|
||||||
TLSConfig: opts.TLSConfig,
|
if err != nil {
|
||||||
}, 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() {
|
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 {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
h := asynqmon.New(asynqmon.Options{
|
h := asynqmon.New(asynqmon.Options{
|
||||||
RedisConnOpt: redisConnOpt,
|
RedisConnOpt: redisConnOpt,
|
||||||
|
PayloadFormatter: asynqmon.PayloadFormatterFunc(payloadFormatterFunc(cfg)),
|
||||||
|
ResultFormatter: asynqmon.ResultFormatterFunc(resultFormatterFunc(cfg)),
|
||||||
|
PrometheusAddress: cfg.PrometheusServerAddr,
|
||||||
|
ReadOnly: cfg.ReadOnly,
|
||||||
})
|
})
|
||||||
defer h.Close()
|
defer h.Close()
|
||||||
|
|
||||||
c := cors.New(cors.Options{
|
c := cors.New(cors.Options{
|
||||||
AllowedMethods: []string{"GET", "POST", "DELETE"},
|
AllowedMethods: []string{"GET", "POST", "DELETE"},
|
||||||
})
|
})
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("/", c.Handler(h))
|
||||||
|
if cfg.EnableMetricsExporter {
|
||||||
|
// Using NewPedanticRegistry here to test the implementation of Collectors and Metrics.
|
||||||
|
reg := prometheus.NewPedanticRegistry()
|
||||||
|
|
||||||
|
inspector := asynq.NewInspector(redisConnOpt)
|
||||||
|
|
||||||
|
reg.MustRegister(
|
||||||
|
metrics.NewQueueMetricsCollector(inspector),
|
||||||
|
// Add the standard process and go metrics to the registry
|
||||||
|
prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}),
|
||||||
|
prometheus.NewGoCollector(),
|
||||||
|
)
|
||||||
|
mux.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
|
||||||
|
}
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Handler: c.Handler(h),
|
Handler: mux,
|
||||||
Addr: fmt.Sprintf(":%d", flagPort),
|
Addr: fmt.Sprintf(":%d", cfg.Port),
|
||||||
WriteTimeout: 10 * time.Second,
|
WriteTimeout: 10 * time.Second,
|
||||||
ReadTimeout: 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())
|
log.Fatal(srv.ListenAndServe())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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).
|
||||||
|
func truncate(s string, limit int) string {
|
||||||
|
i := 0
|
||||||
|
for pos := range s {
|
||||||
|
if i == limit {
|
||||||
|
return s[:pos] + "…"
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvDefaultString(key, def string) string {
|
||||||
|
v := os.Getenv(key)
|
||||||
|
if v == "" {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvOrDefaultInt(key string, def int) int {
|
||||||
|
v, err := strconv.Atoi(os.Getenv(key))
|
||||||
|
if err != nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvOrDefaultBool(key string, def bool) bool {
|
||||||
|
v, err := strconv.ParseBool(os.Getenv(key))
|
||||||
|
if err != nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
137
cmd/asynqmon/main_test.go
Normal file
137
cmd/asynqmon/main_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,7 @@ import (
|
|||||||
// - conversion function from an external type to an internal type
|
// - conversion function from an external type to an internal type
|
||||||
// ****************************************************************************
|
// ****************************************************************************
|
||||||
|
|
||||||
// PayloadFormatter is used to convert payload bytes to string shown in the UI.
|
// PayloadFormatter is used to convert payload bytes to a string shown in the UI.
|
||||||
type PayloadFormatter interface {
|
type PayloadFormatter interface {
|
||||||
// FormatPayload takes the task's typename and payload and returns a string representation of the payload.
|
// FormatPayload takes the task's typename and payload and returns a string representation of the payload.
|
||||||
FormatPayload(taskType string, payload []byte) string
|
FormatPayload(taskType string, payload []byte) string
|
||||||
@ -22,18 +22,42 @@ type PayloadFormatter interface {
|
|||||||
|
|
||||||
type PayloadFormatterFunc func(string, []byte) string
|
type PayloadFormatterFunc func(string, []byte) string
|
||||||
|
|
||||||
// FormatPayload returns a string representation of the payload of the given taskType.
|
|
||||||
func (f PayloadFormatterFunc) FormatPayload(taskType string, payload []byte) string {
|
func (f PayloadFormatterFunc) FormatPayload(taskType string, payload []byte) string {
|
||||||
return f(taskType, payload)
|
return f(taskType, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultPayloadFormatter = PayloadFormatterFunc(func(_ string, payload []byte) string {
|
// ResultFormatter is used to convert result bytes to a string shown in the UI.
|
||||||
|
type ResultFormatter interface {
|
||||||
|
// FormatResult takes the task's typename and result and returns a string representation of the result.
|
||||||
|
FormatResult(taskType string, result []byte) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResultFormatterFunc func(string, []byte) string
|
||||||
|
|
||||||
|
func (f ResultFormatterFunc) FormatResult(taskType string, result []byte) string {
|
||||||
|
return f(taskType, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultPayloadFormatter is the PayloadFormater used by default.
|
||||||
|
// It prints the given payload bytes as is if the bytes are printable, otherwise it prints a message to indicate
|
||||||
|
// that the bytes are not printable.
|
||||||
|
var DefaultPayloadFormatter = PayloadFormatterFunc(func(_ string, payload []byte) string {
|
||||||
if !isPrintable(payload) {
|
if !isPrintable(payload) {
|
||||||
return "non-printable bytes"
|
return "non-printable bytes"
|
||||||
}
|
}
|
||||||
return string(payload)
|
return string(payload)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// DefaultResultFormatter is the ResultFormatter used by default.
|
||||||
|
// It prints the given result bytes as is if the bytes are printable, otherwise it prints a message to indicate
|
||||||
|
// that the bytes are not printable.
|
||||||
|
var DefaultResultFormatter = ResultFormatterFunc(func(_ string, result []byte) string {
|
||||||
|
if !isPrintable(result) {
|
||||||
|
return "non-printable bytes"
|
||||||
|
}
|
||||||
|
return string(result)
|
||||||
|
})
|
||||||
|
|
||||||
// isPrintable reports whether the given data is comprised of all printable runes.
|
// isPrintable reports whether the given data is comprised of all printable runes.
|
||||||
func isPrintable(data []byte) bool {
|
func isPrintable(data []byte) bool {
|
||||||
if !utf8.Valid(data) {
|
if !utf8.Valid(data) {
|
||||||
@ -58,12 +82,21 @@ type queueStateSnapshot struct {
|
|||||||
MemoryUsage int64 `json:"memory_usage_bytes"`
|
MemoryUsage int64 `json:"memory_usage_bytes"`
|
||||||
// Total number of tasks in the queue.
|
// Total number of tasks in the queue.
|
||||||
Size int `json:"size"`
|
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.
|
// Number of tasks in each state.
|
||||||
Active int `json:"active"`
|
Active int `json:"active"`
|
||||||
Pending int `json:"pending"`
|
Pending int `json:"pending"`
|
||||||
Scheduled int `json:"scheduled"`
|
Aggregating int `json:"aggregating"`
|
||||||
Retry int `json:"retry"`
|
Scheduled int `json:"scheduled"`
|
||||||
Archived int `json:"archived"`
|
Retry int `json:"retry"`
|
||||||
|
Archived int `json:"archived"`
|
||||||
|
Completed int `json:"completed"`
|
||||||
|
|
||||||
// Total number of tasks processed during the given date.
|
// Total number of tasks processed during the given date.
|
||||||
// The number includes both succeeded and failed tasks.
|
// The number includes both succeeded and failed tasks.
|
||||||
@ -78,21 +111,26 @@ type queueStateSnapshot struct {
|
|||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func toQueueStateSnapshot(s *asynq.QueueInfo) *queueStateSnapshot {
|
func toQueueStateSnapshot(info *asynq.QueueInfo) *queueStateSnapshot {
|
||||||
return &queueStateSnapshot{
|
return &queueStateSnapshot{
|
||||||
Queue: s.Queue,
|
Queue: info.Queue,
|
||||||
MemoryUsage: s.MemoryUsage,
|
MemoryUsage: info.MemoryUsage,
|
||||||
Size: s.Size,
|
Size: info.Size,
|
||||||
Active: s.Active,
|
Groups: info.Groups,
|
||||||
Pending: s.Pending,
|
LatencyMillisec: info.Latency.Milliseconds(),
|
||||||
Scheduled: s.Scheduled,
|
DisplayLatency: info.Latency.Round(10 * time.Millisecond).String(),
|
||||||
Retry: s.Retry,
|
Active: info.Active,
|
||||||
Archived: s.Archived,
|
Pending: info.Pending,
|
||||||
Processed: s.Processed,
|
Aggregating: info.Aggregating,
|
||||||
Succeeded: s.Processed - s.Failed,
|
Scheduled: info.Scheduled,
|
||||||
Failed: s.Failed,
|
Retry: info.Retry,
|
||||||
Paused: s.Paused,
|
Archived: info.Archived,
|
||||||
Timestamp: s.Timestamp,
|
Completed: info.Completed,
|
||||||
|
Processed: info.Processed,
|
||||||
|
Succeeded: info.Processed - info.Failed,
|
||||||
|
Failed: info.Failed,
|
||||||
|
Paused: info.Paused,
|
||||||
|
Timestamp: info.Timestamp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +187,22 @@ type taskInfo struct {
|
|||||||
// NextProcessAt is the time the task is scheduled to be processed in RFC3339 format.
|
// NextProcessAt is the time the task is scheduled to be processed in RFC3339 format.
|
||||||
// If not applicable, empty string.
|
// If not applicable, empty string.
|
||||||
NextProcessAt string `json:"next_process_at"`
|
NextProcessAt string `json:"next_process_at"`
|
||||||
|
// CompletedAt is the time the task was successfully processed in RFC3339 format.
|
||||||
|
// If not applicable, empty string.
|
||||||
|
CompletedAt string `json:"completed_at"`
|
||||||
|
// Result is the result data associated with the task.
|
||||||
|
Result string `json:"result"`
|
||||||
|
// TTL is the number of seconds the task has left to be retained in the queue.
|
||||||
|
// This is calculated by (CompletedAt + ResultTTL) - Now.
|
||||||
|
TTL int64 `json:"ttl_seconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// taskTTL calculates TTL for the given task.
|
||||||
|
func taskTTL(task *asynq.TaskInfo) time.Duration {
|
||||||
|
if task.State != asynq.TaskStateCompleted {
|
||||||
|
return 0 // N/A
|
||||||
|
}
|
||||||
|
return task.CompletedAt.Add(task.Retention).Sub(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatTimeInRFC3339 formats t in RFC3339 if the value is non-zero.
|
// formatTimeInRFC3339 formats t in RFC3339 if the value is non-zero.
|
||||||
@ -160,7 +214,7 @@ func formatTimeInRFC3339(t time.Time) string {
|
|||||||
return t.Format(time.RFC3339)
|
return t.Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
|
|
||||||
func toTaskInfo(info *asynq.TaskInfo, pf PayloadFormatter) *taskInfo {
|
func toTaskInfo(info *asynq.TaskInfo, pf PayloadFormatter, rf ResultFormatter) *taskInfo {
|
||||||
return &taskInfo{
|
return &taskInfo{
|
||||||
ID: info.ID,
|
ID: info.ID,
|
||||||
Queue: info.Queue,
|
Queue: info.Queue,
|
||||||
@ -174,6 +228,9 @@ func toTaskInfo(info *asynq.TaskInfo, pf PayloadFormatter) *taskInfo {
|
|||||||
Timeout: int(info.Timeout.Seconds()),
|
Timeout: int(info.Timeout.Seconds()),
|
||||||
Deadline: formatTimeInRFC3339(info.Deadline),
|
Deadline: formatTimeInRFC3339(info.Deadline),
|
||||||
NextProcessAt: formatTimeInRFC3339(info.NextProcessAt),
|
NextProcessAt: formatTimeInRFC3339(info.NextProcessAt),
|
||||||
|
CompletedAt: formatTimeInRFC3339(info.CompletedAt),
|
||||||
|
Result: rf.FormatResult("", info.Result),
|
||||||
|
TTL: int64(taskTTL(info).Seconds()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,6 +259,9 @@ type activeTask struct {
|
|||||||
// Value is either time formatted in RFC3339 format, or "-" which indicates that
|
// Value is either time formatted in RFC3339 format, or "-" which indicates that
|
||||||
// the data is not available yet.
|
// the data is not available yet.
|
||||||
Deadline string `json:"deadline"`
|
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 {
|
func toActiveTask(ti *asynq.TaskInfo, pf PayloadFormatter) *activeTask {
|
||||||
@ -214,7 +274,7 @@ func toActiveTask(ti *asynq.TaskInfo, pf PayloadFormatter) *activeTask {
|
|||||||
Retried: ti.Retried,
|
Retried: ti.Retried,
|
||||||
LastError: ti.LastErr,
|
LastError: ti.LastErr,
|
||||||
}
|
}
|
||||||
return &activeTask{baseTask: base}
|
return &activeTask{baseTask: base, IsOrphaned: ti.IsOrphaned}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toActiveTasks(in []*asynq.TaskInfo, pf PayloadFormatter) []*activeTask {
|
func toActiveTasks(in []*asynq.TaskInfo, pf PayloadFormatter) []*activeTask {
|
||||||
@ -253,6 +313,35 @@ func toPendingTasks(in []*asynq.TaskInfo, pf PayloadFormatter) []*pendingTask {
|
|||||||
return out
|
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 {
|
type scheduledTask struct {
|
||||||
*baseTask
|
*baseTask
|
||||||
NextProcessAt time.Time `json:"next_process_at"`
|
NextProcessAt time.Time `json:"next_process_at"`
|
||||||
@ -340,6 +429,60 @@ func toArchivedTasks(in []*asynq.TaskInfo, pf PayloadFormatter) []*archivedTask
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type completedTask struct {
|
||||||
|
*baseTask
|
||||||
|
CompletedAt time.Time `json:"completed_at"`
|
||||||
|
Result string `json:"result"`
|
||||||
|
// Number of seconds left for retention (i.e. (CompletedAt + ResultTTL) - Now)
|
||||||
|
TTL int64 `json:"ttl_seconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func toCompletedTask(ti *asynq.TaskInfo, pf PayloadFormatter, rf ResultFormatter) *completedTask {
|
||||||
|
base := &baseTask{
|
||||||
|
ID: ti.ID,
|
||||||
|
Type: ti.Type,
|
||||||
|
Payload: pf.FormatPayload(ti.Type, ti.Payload),
|
||||||
|
Queue: ti.Queue,
|
||||||
|
MaxRetry: ti.MaxRetry,
|
||||||
|
Retried: ti.Retried,
|
||||||
|
LastError: ti.LastErr,
|
||||||
|
}
|
||||||
|
return &completedTask{
|
||||||
|
baseTask: base,
|
||||||
|
CompletedAt: ti.CompletedAt,
|
||||||
|
TTL: int64(taskTTL(ti).Seconds()),
|
||||||
|
Result: rf.FormatResult(ti.Type, ti.Result),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toCompletedTasks(in []*asynq.TaskInfo, pf PayloadFormatter, rf ResultFormatter) []*completedTask {
|
||||||
|
out := make([]*completedTask, len(in))
|
||||||
|
for i, ti := range in {
|
||||||
|
out[i] = toCompletedTask(ti, pf, rf)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
type 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 {
|
type schedulerEntry struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Spec string `json:"spec"`
|
Spec string `json:"spec"`
|
||||||
|
12
go.mod
12
go.mod
@ -3,8 +3,16 @@ module github.com/hibiken/asynqmon
|
|||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-redis/redis/v8 v8.11.3
|
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/gorilla/mux v1.8.0
|
||||||
github.com/hibiken/asynq v0.18.6
|
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/rs/cors v1.7.0
|
||||||
|
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
|
||||||
)
|
)
|
||||||
|
198
go.sum
198
go.sum
@ -1,35 +1,51 @@
|
|||||||
cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=
|
|
||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
|
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/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.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/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 h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
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 h1:4cmBvAEBNJaGARUEs3/suWRyfyBfhf7I60WBZq+bv2w=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
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 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
|
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
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.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/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=
|
||||||
|
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
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.2/go.mod h1:DLomh7y2e3ggQXQLd1YgmvIfecPJoFl7WU5SOQ/r06M=
|
||||||
github.com/go-redis/redis/v8 v8.11.3 h1:GCjoYp8c+yQTJfc0n69iwSiHjvuAdruxl7elnZCxgt8=
|
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
|
||||||
github.com/go-redis/redis/v8 v8.11.3/go.mod h1:xNJ9xDG09FsIPwh3bWdk+0oDWHbtF9rPN0F/oD9XeKc=
|
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 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
|
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
@ -38,139 +54,209 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
|
|||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
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.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.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.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.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
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.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.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
|
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.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 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/hibiken/asynq v0.18.6 h1:pBjtGh2QhDe1+/0yaSc56ANpdQ77BQgVfMIrj+NJrUM=
|
github.com/hibiken/asynq v0.19.0/go.mod h1:tyc63ojaW8SJ5SBm8mvI4DDONsguP5HE85EEl4Qr5Ig=
|
||||||
github.com/hibiken/asynq v0.18.6/go.mod h1:tyc63ojaW8SJ5SBm8mvI4DDONsguP5HE85EEl4Qr5Ig=
|
github.com/hibiken/asynq v0.24.1 h1:+5iIEAyA9K/lcSPvx3qoPtsKJeKI5u9aOIvUmSsazEw=
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
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=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||||
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
|
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
|
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/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
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=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
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.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/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.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.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
|
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/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.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.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.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
|
||||||
github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU=
|
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||||
github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
|
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/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=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||||
|
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||||
|
github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ=
|
||||||
|
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
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 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
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 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
||||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
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/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
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.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.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.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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
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 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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=
|
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=
|
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
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.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-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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
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-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-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/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
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=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
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-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-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=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
|
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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
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/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/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.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.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/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-20190308202827-9d24e82272b4/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-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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e h1:4nW4NLDYnU28ojHaHO8OVxFHk/aQ33U01a9cjED+pzE=
|
|
||||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
|
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
|
|
||||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
@ -182,22 +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.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.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-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.26.0/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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
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/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
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/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/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.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.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.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.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=
|
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-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=
|
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/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
40
group_handlers.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
97
handler.go
97
handler.go
@ -6,13 +6,12 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/hibiken/asynq"
|
"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 {
|
type Options struct {
|
||||||
// URL path the handler is responsible for.
|
// 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.
|
// The path is used for the homepage of asynqmon, and every other page is rooted in this subtree.
|
||||||
@ -29,6 +28,20 @@ type Options struct {
|
|||||||
//
|
//
|
||||||
// This field is optional.
|
// This field is optional.
|
||||||
PayloadFormatter PayloadFormatter
|
PayloadFormatter PayloadFormatter
|
||||||
|
|
||||||
|
// ResultFormatter is used to convert result bytes to string shown in the UI.
|
||||||
|
//
|
||||||
|
// This field is optional.
|
||||||
|
ResultFormatter ResultFormatter
|
||||||
|
|
||||||
|
// PrometheusAddress specifies the address of the Prometheus to connect to.
|
||||||
|
//
|
||||||
|
// 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.
|
// HTTPHandler is a http.Handler for asynqmon application.
|
||||||
@ -78,8 +91,9 @@ func (h *HTTPHandler) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RootPath returns the root URL path used for asynqmon application.
|
// RootPath returns the root URL path used for asynqmon application.
|
||||||
|
// Returned path string does not have the trailing slash.
|
||||||
func (h *HTTPHandler) RootPath() string {
|
func (h *HTTPHandler) RootPath() string {
|
||||||
return h.rootPath + "/"
|
return h.rootPath
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed ui/build/*
|
//go:embed ui/build/*
|
||||||
@ -88,12 +102,18 @@ var staticContents embed.FS
|
|||||||
func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspector) *mux.Router {
|
func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspector) *mux.Router {
|
||||||
router := mux.NewRouter().PathPrefix(opts.RootPath).Subrouter()
|
router := mux.NewRouter().PathPrefix(opts.RootPath).Subrouter()
|
||||||
|
|
||||||
var pf PayloadFormatter = defaultPayloadFormatter
|
var payloadFmt PayloadFormatter = DefaultPayloadFormatter
|
||||||
if opts.PayloadFormatter != nil {
|
if opts.PayloadFormatter != nil {
|
||||||
pf = opts.PayloadFormatter
|
payloadFmt = opts.PayloadFormatter
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultFmt ResultFormatter = DefaultResultFormatter
|
||||||
|
if opts.ResultFormatter != nil {
|
||||||
|
resultFmt = opts.ResultFormatter
|
||||||
}
|
}
|
||||||
|
|
||||||
api := router.PathPrefix("/api").Subrouter()
|
api := router.PathPrefix("/api").Subrouter()
|
||||||
|
|
||||||
// Queue endpoints.
|
// Queue endpoints.
|
||||||
api.HandleFunc("/queues", newListQueuesHandlerFunc(inspector)).Methods("GET")
|
api.HandleFunc("/queues", newListQueuesHandlerFunc(inspector)).Methods("GET")
|
||||||
api.HandleFunc("/queues/{qname}", newGetQueueHandlerFunc(inspector)).Methods("GET")
|
api.HandleFunc("/queues/{qname}", newGetQueueHandlerFunc(inspector)).Methods("GET")
|
||||||
@ -105,12 +125,12 @@ func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspecto
|
|||||||
api.HandleFunc("/queue_stats", newListQueueStatsHandlerFunc(inspector)).Methods("GET")
|
api.HandleFunc("/queue_stats", newListQueueStatsHandlerFunc(inspector)).Methods("GET")
|
||||||
|
|
||||||
// Task endpoints.
|
// Task endpoints.
|
||||||
api.HandleFunc("/queues/{qname}/active_tasks", newListActiveTasksHandlerFunc(inspector, pf)).Methods("GET")
|
api.HandleFunc("/queues/{qname}/active_tasks", newListActiveTasksHandlerFunc(inspector, payloadFmt)).Methods("GET")
|
||||||
api.HandleFunc("/queues/{qname}/active_tasks/{task_id}:cancel", newCancelActiveTaskHandlerFunc(inspector)).Methods("POST")
|
api.HandleFunc("/queues/{qname}/active_tasks/{task_id}:cancel", newCancelActiveTaskHandlerFunc(inspector)).Methods("POST")
|
||||||
api.HandleFunc("/queues/{qname}/active_tasks:cancel_all", newCancelAllActiveTasksHandlerFunc(inspector)).Methods("POST")
|
api.HandleFunc("/queues/{qname}/active_tasks:cancel_all", newCancelAllActiveTasksHandlerFunc(inspector)).Methods("POST")
|
||||||
api.HandleFunc("/queues/{qname}/active_tasks:batch_cancel", newBatchCancelActiveTasksHandlerFunc(inspector)).Methods("POST")
|
api.HandleFunc("/queues/{qname}/active_tasks:batch_cancel", newBatchCancelActiveTasksHandlerFunc(inspector)).Methods("POST")
|
||||||
|
|
||||||
api.HandleFunc("/queues/{qname}/pending_tasks", newListPendingTasksHandlerFunc(inspector, pf)).Methods("GET")
|
api.HandleFunc("/queues/{qname}/pending_tasks", newListPendingTasksHandlerFunc(inspector, payloadFmt)).Methods("GET")
|
||||||
api.HandleFunc("/queues/{qname}/pending_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE")
|
api.HandleFunc("/queues/{qname}/pending_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE")
|
||||||
api.HandleFunc("/queues/{qname}/pending_tasks:delete_all", newDeleteAllPendingTasksHandlerFunc(inspector)).Methods("DELETE")
|
api.HandleFunc("/queues/{qname}/pending_tasks:delete_all", newDeleteAllPendingTasksHandlerFunc(inspector)).Methods("DELETE")
|
||||||
api.HandleFunc("/queues/{qname}/pending_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST")
|
api.HandleFunc("/queues/{qname}/pending_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST")
|
||||||
@ -118,7 +138,7 @@ func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspecto
|
|||||||
api.HandleFunc("/queues/{qname}/pending_tasks:archive_all", newArchiveAllPendingTasksHandlerFunc(inspector)).Methods("POST")
|
api.HandleFunc("/queues/{qname}/pending_tasks:archive_all", newArchiveAllPendingTasksHandlerFunc(inspector)).Methods("POST")
|
||||||
api.HandleFunc("/queues/{qname}/pending_tasks:batch_archive", newBatchArchiveTasksHandlerFunc(inspector)).Methods("POST")
|
api.HandleFunc("/queues/{qname}/pending_tasks:batch_archive", newBatchArchiveTasksHandlerFunc(inspector)).Methods("POST")
|
||||||
|
|
||||||
api.HandleFunc("/queues/{qname}/scheduled_tasks", newListScheduledTasksHandlerFunc(inspector, pf)).Methods("GET")
|
api.HandleFunc("/queues/{qname}/scheduled_tasks", newListScheduledTasksHandlerFunc(inspector, payloadFmt)).Methods("GET")
|
||||||
api.HandleFunc("/queues/{qname}/scheduled_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE")
|
api.HandleFunc("/queues/{qname}/scheduled_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE")
|
||||||
api.HandleFunc("/queues/{qname}/scheduled_tasks:delete_all", newDeleteAllScheduledTasksHandlerFunc(inspector)).Methods("DELETE")
|
api.HandleFunc("/queues/{qname}/scheduled_tasks:delete_all", newDeleteAllScheduledTasksHandlerFunc(inspector)).Methods("DELETE")
|
||||||
api.HandleFunc("/queues/{qname}/scheduled_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST")
|
api.HandleFunc("/queues/{qname}/scheduled_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST")
|
||||||
@ -129,7 +149,7 @@ func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspecto
|
|||||||
api.HandleFunc("/queues/{qname}/scheduled_tasks:archive_all", newArchiveAllScheduledTasksHandlerFunc(inspector)).Methods("POST")
|
api.HandleFunc("/queues/{qname}/scheduled_tasks:archive_all", newArchiveAllScheduledTasksHandlerFunc(inspector)).Methods("POST")
|
||||||
api.HandleFunc("/queues/{qname}/scheduled_tasks:batch_archive", newBatchArchiveTasksHandlerFunc(inspector)).Methods("POST")
|
api.HandleFunc("/queues/{qname}/scheduled_tasks:batch_archive", newBatchArchiveTasksHandlerFunc(inspector)).Methods("POST")
|
||||||
|
|
||||||
api.HandleFunc("/queues/{qname}/retry_tasks", newListRetryTasksHandlerFunc(inspector, pf)).Methods("GET")
|
api.HandleFunc("/queues/{qname}/retry_tasks", newListRetryTasksHandlerFunc(inspector, payloadFmt)).Methods("GET")
|
||||||
api.HandleFunc("/queues/{qname}/retry_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE")
|
api.HandleFunc("/queues/{qname}/retry_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE")
|
||||||
api.HandleFunc("/queues/{qname}/retry_tasks:delete_all", newDeleteAllRetryTasksHandlerFunc(inspector)).Methods("DELETE")
|
api.HandleFunc("/queues/{qname}/retry_tasks:delete_all", newDeleteAllRetryTasksHandlerFunc(inspector)).Methods("DELETE")
|
||||||
api.HandleFunc("/queues/{qname}/retry_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST")
|
api.HandleFunc("/queues/{qname}/retry_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST")
|
||||||
@ -140,7 +160,7 @@ func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspecto
|
|||||||
api.HandleFunc("/queues/{qname}/retry_tasks:archive_all", newArchiveAllRetryTasksHandlerFunc(inspector)).Methods("POST")
|
api.HandleFunc("/queues/{qname}/retry_tasks:archive_all", newArchiveAllRetryTasksHandlerFunc(inspector)).Methods("POST")
|
||||||
api.HandleFunc("/queues/{qname}/retry_tasks:batch_archive", newBatchArchiveTasksHandlerFunc(inspector)).Methods("POST")
|
api.HandleFunc("/queues/{qname}/retry_tasks:batch_archive", newBatchArchiveTasksHandlerFunc(inspector)).Methods("POST")
|
||||||
|
|
||||||
api.HandleFunc("/queues/{qname}/archived_tasks", newListArchivedTasksHandlerFunc(inspector, pf)).Methods("GET")
|
api.HandleFunc("/queues/{qname}/archived_tasks", newListArchivedTasksHandlerFunc(inspector, payloadFmt)).Methods("GET")
|
||||||
api.HandleFunc("/queues/{qname}/archived_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE")
|
api.HandleFunc("/queues/{qname}/archived_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE")
|
||||||
api.HandleFunc("/queues/{qname}/archived_tasks:delete_all", newDeleteAllArchivedTasksHandlerFunc(inspector)).Methods("DELETE")
|
api.HandleFunc("/queues/{qname}/archived_tasks:delete_all", newDeleteAllArchivedTasksHandlerFunc(inspector)).Methods("DELETE")
|
||||||
api.HandleFunc("/queues/{qname}/archived_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST")
|
api.HandleFunc("/queues/{qname}/archived_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST")
|
||||||
@ -148,13 +168,32 @@ func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspecto
|
|||||||
api.HandleFunc("/queues/{qname}/archived_tasks:run_all", newRunAllArchivedTasksHandlerFunc(inspector)).Methods("POST")
|
api.HandleFunc("/queues/{qname}/archived_tasks:run_all", newRunAllArchivedTasksHandlerFunc(inspector)).Methods("POST")
|
||||||
api.HandleFunc("/queues/{qname}/archived_tasks:batch_run", newBatchRunTasksHandlerFunc(inspector)).Methods("POST")
|
api.HandleFunc("/queues/{qname}/archived_tasks:batch_run", newBatchRunTasksHandlerFunc(inspector)).Methods("POST")
|
||||||
|
|
||||||
api.HandleFunc("/queues/{qname}/tasks/{task_id}", newGetTaskHandlerFunc(inspector, pf)).Methods("GET")
|
api.HandleFunc("/queues/{qname}/completed_tasks", newListCompletedTasksHandlerFunc(inspector, payloadFmt, resultFmt)).Methods("GET")
|
||||||
|
api.HandleFunc("/queues/{qname}/completed_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE")
|
||||||
|
api.HandleFunc("/queues/{qname}/completed_tasks:delete_all", newDeleteAllCompletedTasksHandlerFunc(inspector)).Methods("DELETE")
|
||||||
|
api.HandleFunc("/queues/{qname}/completed_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST")
|
||||||
|
|
||||||
|
api.HandleFunc("/queues/{qname}/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.
|
// Servers endpoints.
|
||||||
api.HandleFunc("/servers", newListServersHandlerFunc(inspector, pf)).Methods("GET")
|
api.HandleFunc("/servers", newListServersHandlerFunc(inspector, payloadFmt)).Methods("GET")
|
||||||
|
|
||||||
// Scheduler Entry endpoints.
|
// Scheduler Entry endpoints.
|
||||||
api.HandleFunc("/scheduler_entries", newListSchedulerEntriesHandlerFunc(inspector, pf)).Methods("GET")
|
api.HandleFunc("/scheduler_entries", newListSchedulerEntriesHandlerFunc(inspector, payloadFmt)).Methods("GET")
|
||||||
api.HandleFunc("/scheduler_entries/{entry_id}/enqueue_events", newListSchedulerEnqueueEventsHandlerFunc(inspector)).Methods("GET")
|
api.HandleFunc("/scheduler_entries/{entry_id}/enqueue_events", newListSchedulerEnqueueEventsHandlerFunc(inspector)).Methods("GET")
|
||||||
|
|
||||||
// Redis info endpoint.
|
// Redis info endpoint.
|
||||||
@ -165,12 +204,34 @@ func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspecto
|
|||||||
api.HandleFunc("/redis_info", newRedisInfoHandlerFunc(c)).Methods("GET")
|
api.HandleFunc("/redis_info", newRedisInfoHandlerFunc(c)).Methods("GET")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
// Everything else, route to uiAssetsHandler.
|
||||||
router.NotFoundHandler = &uiAssetsHandler{
|
router.NotFoundHandler = &uiAssetsHandler{
|
||||||
rootPath: opts.RootPath,
|
rootPath: opts.RootPath,
|
||||||
contents: staticContents,
|
contents: staticContents,
|
||||||
staticDirPath: "ui/build",
|
staticDirPath: "ui/build",
|
||||||
indexFileName: "index.html",
|
indexFileName: "index.html",
|
||||||
|
prometheusAddr: opts.PrometheusAddress,
|
||||||
|
readOnly: opts.ReadOnly,
|
||||||
}
|
}
|
||||||
|
|
||||||
return router
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
230
metrics_handler.go
Normal file
230
metrics_handler.go
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
package asynqmon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type getMetricsResponse struct {
|
||||||
|
QueueSize *json.RawMessage `json:"queue_size"`
|
||||||
|
QueueLatency *json.RawMessage `json:"queue_latency_seconds"`
|
||||||
|
QueueMemUsgApprox *json.RawMessage `json:"queue_memory_usage_approx_bytes"`
|
||||||
|
ProcessedPerSecond *json.RawMessage `json:"tasks_processed_per_second"`
|
||||||
|
FailedPerSecond *json.RawMessage `json:"tasks_failed_per_second"`
|
||||||
|
ErrorRate *json.RawMessage `json:"error_rate"`
|
||||||
|
PendingTasksByQueue *json.RawMessage `json:"pending_tasks_by_queue"`
|
||||||
|
RetryTasksByQueue *json.RawMessage `json:"retry_tasks_by_queue"`
|
||||||
|
ArchivedTasksByQueue *json.RawMessage `json:"archived_tasks_by_queue"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type metricsFetchOptions struct {
|
||||||
|
// Specifies the number of seconds to scan for metrics.
|
||||||
|
duration time.Duration
|
||||||
|
|
||||||
|
// Specifies the end time when fetching metrics.
|
||||||
|
endTime time.Time
|
||||||
|
|
||||||
|
// Optional filter to speicify a list of queues to get metrics for.
|
||||||
|
// Empty list indicates no filter (i.e. get metrics for all queues).
|
||||||
|
queues []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGetMetricsHandlerFunc(client *http.Client, prometheusAddr string) http.HandlerFunc {
|
||||||
|
// res is the result of calling a JSON API endpoint.
|
||||||
|
type res struct {
|
||||||
|
query string
|
||||||
|
msg *json.RawMessage
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// List of PromQLs.
|
||||||
|
// Strings are used as template to optionally insert queue filter specified by QUEUE_FILTER.
|
||||||
|
const (
|
||||||
|
promQLQueueSize = "asynq_queue_size{QUEUE_FILTER}"
|
||||||
|
promQLQueueLatency = "asynq_queue_latency_seconds{QUEUE_FILTER}"
|
||||||
|
promQLMemUsage = "asynq_queue_memory_usage_approx_bytes{QUEUE_FILTER}"
|
||||||
|
promQLProcessedTasks = "rate(asynq_tasks_processed_total{QUEUE_FILTER}[5m])"
|
||||||
|
promQLFailedTasks = "rate(asynq_tasks_failed_total{QUEUE_FILTER}[5m])"
|
||||||
|
promQLErrorRate = "rate(asynq_tasks_failed_total{QUEUE_FILTER}[5m]) / rate(asynq_tasks_processed_total{QUEUE_FILTER}[5m])"
|
||||||
|
promQLPendingTasks = "asynq_tasks_enqueued_total{state=\"pending\",QUEUE_FILTER}"
|
||||||
|
promQLRetryTasks = "asynq_tasks_enqueued_total{state=\"retry\",QUEUE_FILTER}"
|
||||||
|
promQLArchivedTasks = "asynq_tasks_enqueued_total{state=\"archived\",QUEUE_FILTER}"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Optional query params:
|
||||||
|
// `duration_sec`: specifies the number of seconds to scan
|
||||||
|
// `end_time`: specifies the end_time in Unix time seconds
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
opts, err := extractMetricsFetchOptions(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("invalid query parameter: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// List of queries (i.e. promQL) to send to prometheus server.
|
||||||
|
queries := []string{
|
||||||
|
promQLQueueSize,
|
||||||
|
promQLQueueLatency,
|
||||||
|
promQLMemUsage,
|
||||||
|
promQLProcessedTasks,
|
||||||
|
promQLFailedTasks,
|
||||||
|
promQLErrorRate,
|
||||||
|
promQLPendingTasks,
|
||||||
|
promQLRetryTasks,
|
||||||
|
promQLArchivedTasks,
|
||||||
|
}
|
||||||
|
resp := getMetricsResponse{}
|
||||||
|
// Make multiple API calls concurrently
|
||||||
|
n := len(queries)
|
||||||
|
ch := make(chan res, len(queries))
|
||||||
|
for _, q := range queries {
|
||||||
|
go func(q string) {
|
||||||
|
url := buildPrometheusURL(prometheusAddr, q, opts)
|
||||||
|
msg, err := fetchPrometheusMetrics(client, url)
|
||||||
|
ch <- res{q, msg, err}
|
||||||
|
}(q)
|
||||||
|
}
|
||||||
|
for r := range ch {
|
||||||
|
n--
|
||||||
|
if r.err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to fetch %q: %v", r.query, r.err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch r.query {
|
||||||
|
case promQLQueueSize:
|
||||||
|
resp.QueueSize = r.msg
|
||||||
|
case promQLQueueLatency:
|
||||||
|
resp.QueueLatency = r.msg
|
||||||
|
case promQLMemUsage:
|
||||||
|
resp.QueueMemUsgApprox = r.msg
|
||||||
|
case promQLProcessedTasks:
|
||||||
|
resp.ProcessedPerSecond = r.msg
|
||||||
|
case promQLFailedTasks:
|
||||||
|
resp.FailedPerSecond = r.msg
|
||||||
|
case promQLErrorRate:
|
||||||
|
resp.ErrorRate = r.msg
|
||||||
|
case promQLPendingTasks:
|
||||||
|
resp.PendingTasksByQueue = r.msg
|
||||||
|
case promQLRetryTasks:
|
||||||
|
resp.RetryTasksByQueue = r.msg
|
||||||
|
case promQLArchivedTasks:
|
||||||
|
resp.ArchivedTasksByQueue = r.msg
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
break // fetched all metrics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bytes, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to marshal response into JSON: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := w.Write(bytes); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to write to response: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const prometheusAPIPath = "/api/v1/query_range"
|
||||||
|
|
||||||
|
func extractMetricsFetchOptions(r *http.Request) (*metricsFetchOptions, error) {
|
||||||
|
opts := &metricsFetchOptions{
|
||||||
|
duration: 60 * time.Minute,
|
||||||
|
endTime: time.Now(),
|
||||||
|
}
|
||||||
|
q := r.URL.Query()
|
||||||
|
if d := q.Get("duration"); d != "" {
|
||||||
|
val, err := strconv.Atoi(d)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid value provided for duration: %q", d)
|
||||||
|
}
|
||||||
|
opts.duration = time.Duration(val) * time.Second
|
||||||
|
}
|
||||||
|
if t := q.Get("endtime"); t != "" {
|
||||||
|
val, err := strconv.Atoi(t)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid value provided for end_time: %q", t)
|
||||||
|
}
|
||||||
|
opts.endTime = time.Unix(int64(val), 0)
|
||||||
|
}
|
||||||
|
if qs := q.Get("queues"); qs != "" {
|
||||||
|
opts.queues = strings.Split(qs, ",")
|
||||||
|
}
|
||||||
|
return opts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPrometheusURL(baseAddr, promQL string, opts *metricsFetchOptions) string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(strings.TrimSuffix(baseAddr, "/"))
|
||||||
|
b.WriteString(prometheusAPIPath)
|
||||||
|
v := url.Values{}
|
||||||
|
v.Add("query", applyQueueFilter(promQL, opts.queues))
|
||||||
|
v.Add("start", unixTimeString(opts.endTime.Add(-opts.duration)))
|
||||||
|
v.Add("end", unixTimeString(opts.endTime))
|
||||||
|
v.Add("step", strconv.Itoa(int(step(opts).Seconds())))
|
||||||
|
b.WriteString("?")
|
||||||
|
b.WriteString(v.Encode())
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyQueueFilter(promQL string, qnames []string) string {
|
||||||
|
if len(qnames) == 0 {
|
||||||
|
return strings.ReplaceAll(promQL, "QUEUE_FILTER", "")
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(`queue=~"`)
|
||||||
|
for i, q := range qnames {
|
||||||
|
if i != 0 {
|
||||||
|
b.WriteString("|")
|
||||||
|
}
|
||||||
|
b.WriteString(q)
|
||||||
|
}
|
||||||
|
b.WriteByte('"')
|
||||||
|
return strings.ReplaceAll(promQL, "QUEUE_FILTER", b.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchPrometheusMetrics(client *http.Client, url string) (*json.RawMessage, error) {
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
bytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
msg := json.RawMessage(bytes)
|
||||||
|
return &msg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns step to use given the fetch options.
|
||||||
|
// In general, the longer the duration, longer the each step.
|
||||||
|
func step(opts *metricsFetchOptions) time.Duration {
|
||||||
|
if opts.duration <= 6*time.Hour {
|
||||||
|
// maximum number of data points to return: 6h / 10s = 2160
|
||||||
|
return 10 * time.Second
|
||||||
|
}
|
||||||
|
if opts.duration <= 24*time.Hour {
|
||||||
|
// maximum number of data points to return: 24h / 1m = 1440
|
||||||
|
return 1 * time.Minute
|
||||||
|
}
|
||||||
|
if opts.duration <= 8*24*time.Hour {
|
||||||
|
// maximum number of data points to return: (8*24)h / 3m = 3840
|
||||||
|
return 3 * time.Minute
|
||||||
|
}
|
||||||
|
if opts.duration <= 30*24*time.Hour {
|
||||||
|
// maximum number of data points to return: (30*24)h / 10m = 4320
|
||||||
|
return 10 * time.Minute
|
||||||
|
}
|
||||||
|
return opts.duration / 3000
|
||||||
|
}
|
||||||
|
|
||||||
|
func unixTimeString(t time.Time) string {
|
||||||
|
return strconv.Itoa(int(t.Unix()))
|
||||||
|
}
|
@ -6,9 +6,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
|
|
||||||
"github.com/hibiken/asynq"
|
"github.com/hibiken/asynq"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ****************************************************************************
|
// ****************************************************************************
|
||||||
|
26
static.go
26
static.go
@ -15,10 +15,12 @@ import (
|
|||||||
// the path to the index file within that static directory are used to
|
// the path to the index file within that static directory are used to
|
||||||
// serve the SPA.
|
// serve the SPA.
|
||||||
type uiAssetsHandler struct {
|
type uiAssetsHandler struct {
|
||||||
rootPath string
|
rootPath string
|
||||||
contents embed.FS
|
contents embed.FS
|
||||||
staticDirPath string
|
staticDirPath string
|
||||||
indexFileName string
|
indexFileName string
|
||||||
|
prometheusAddr string
|
||||||
|
readOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP inspects the URL path to locate a file within the static dir
|
// ServeHTTP inspects the URL path to locate a file within the static dir
|
||||||
@ -59,9 +61,13 @@ func (h *uiAssetsHandler) renderIndexFile(w http.ResponseWriter) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
data := struct {
|
data := struct {
|
||||||
RootPath string
|
RootPath string
|
||||||
|
PrometheusAddr string
|
||||||
|
ReadOnly bool
|
||||||
}{
|
}{
|
||||||
RootPath: h.rootPath,
|
RootPath: h.rootPath,
|
||||||
|
PrometheusAddr: h.prometheusAddr,
|
||||||
|
ReadOnly: h.readOnly,
|
||||||
}
|
}
|
||||||
return tmpl.Execute(w, data)
|
return tmpl.Execute(w, data)
|
||||||
}
|
}
|
||||||
@ -92,6 +98,14 @@ func (h *uiAssetsHandler) serveFile(w http.ResponseWriter, path string) (code in
|
|||||||
}
|
}
|
||||||
return http.StatusInternalServerError, err
|
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 {
|
if _, err := w.Write(bytes); err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
|
217
task_handlers.go
217
task_handlers.go
@ -71,10 +71,7 @@ func newListActiveTasksHandlerFunc(inspector *asynq.Inspector, pf PayloadFormatt
|
|||||||
Tasks: activeTasks,
|
Tasks: activeTasks,
|
||||||
Stats: toQueueStateSnapshot(qinfo),
|
Stats: toQueueStateSnapshot(qinfo),
|
||||||
}
|
}
|
||||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
writeResponseJSON(w, resp)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,10 +146,7 @@ func newBatchCancelActiveTasksHandlerFunc(inspector *asynq.Inspector) http.Handl
|
|||||||
resp.CanceledIDs = append(resp.CanceledIDs, id)
|
resp.CanceledIDs = append(resp.CanceledIDs, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
writeResponseJSON(w, resp)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,10 +174,7 @@ func newListPendingTasksHandlerFunc(inspector *asynq.Inspector, pf PayloadFormat
|
|||||||
payload["tasks"] = toPendingTasks(tasks, pf)
|
payload["tasks"] = toPendingTasks(tasks, pf)
|
||||||
}
|
}
|
||||||
payload["stats"] = toQueueStateSnapshot(qinfo)
|
payload["stats"] = toQueueStateSnapshot(qinfo)
|
||||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
writeResponseJSON(w, payload)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,10 +202,7 @@ func newListScheduledTasksHandlerFunc(inspector *asynq.Inspector, pf PayloadForm
|
|||||||
payload["tasks"] = toScheduledTasks(tasks, pf)
|
payload["tasks"] = toScheduledTasks(tasks, pf)
|
||||||
}
|
}
|
||||||
payload["stats"] = toQueueStateSnapshot(qinfo)
|
payload["stats"] = toQueueStateSnapshot(qinfo)
|
||||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
writeResponseJSON(w, payload)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,10 +230,7 @@ func newListRetryTasksHandlerFunc(inspector *asynq.Inspector, pf PayloadFormatte
|
|||||||
payload["tasks"] = toRetryTasks(tasks, pf)
|
payload["tasks"] = toRetryTasks(tasks, pf)
|
||||||
}
|
}
|
||||||
payload["stats"] = toQueueStateSnapshot(qinfo)
|
payload["stats"] = toQueueStateSnapshot(qinfo)
|
||||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
writeResponseJSON(w, payload)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,10 +258,69 @@ func newListArchivedTasksHandlerFunc(inspector *asynq.Inspector, pf PayloadForma
|
|||||||
payload["tasks"] = toArchivedTasks(tasks, pf)
|
payload["tasks"] = toArchivedTasks(tasks, pf)
|
||||||
}
|
}
|
||||||
payload["stats"] = toQueueStateSnapshot(qinfo)
|
payload["stats"] = toQueueStateSnapshot(qinfo)
|
||||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
writeResponseJSON(w, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newListCompletedTasksHandlerFunc(inspector *asynq.Inspector, pf PayloadFormatter, rf ResultFormatter) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
qname := vars["qname"]
|
||||||
|
pageSize, pageNum := getPageOptions(r)
|
||||||
|
tasks, err := inspector.ListCompletedTasks(qname, asynq.PageSize(pageSize), asynq.Page(pageNum))
|
||||||
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
qinfo, err := inspector.GetQueueInfo(qname)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payload := make(map[string]interface{})
|
||||||
|
if len(tasks) == 0 {
|
||||||
|
// avoid nil for the tasks field in json output.
|
||||||
|
payload["tasks"] = make([]*completedTask, 0)
|
||||||
|
} else {
|
||||||
|
payload["tasks"] = toCompletedTasks(tasks, pf, rf)
|
||||||
|
}
|
||||||
|
payload["stats"] = toQueueStateSnapshot(qinfo)
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,11 +388,20 @@ func newDeleteAllPendingTasksHandlerFunc(inspector *asynq.Inspector) http.Handle
|
|||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp := deleteAllTasksResponse{n}
|
writeResponseJSON(w, deleteAllTasksResponse{n})
|
||||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
writeResponseJSON(w, deleteAllTasksResponse{n})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,11 +413,7 @@ func newDeleteAllScheduledTasksHandlerFunc(inspector *asynq.Inspector) http.Hand
|
|||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp := deleteAllTasksResponse{n}
|
writeResponseJSON(w, deleteAllTasksResponse{n})
|
||||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,11 +425,7 @@ func newDeleteAllRetryTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerF
|
|||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp := deleteAllTasksResponse{n}
|
writeResponseJSON(w, deleteAllTasksResponse{n})
|
||||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -392,77 +437,133 @@ func newDeleteAllArchivedTasksHandlerFunc(inspector *asynq.Inspector) http.Handl
|
|||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp := deleteAllTasksResponse{n}
|
writeResponseJSON(w, deleteAllTasksResponse{n})
|
||||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDeleteAllCompletedTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
qname := mux.Vars(r)["qname"]
|
||||||
|
n, err := inspector.DeleteAllCompletedTasks(qname)
|
||||||
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
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 {
|
func newRunAllScheduledTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
qname := mux.Vars(r)["qname"]
|
qname := mux.Vars(r)["qname"]
|
||||||
if _, err := inspector.RunAllScheduledTasks(qname); err != nil {
|
n, err := inspector.RunAllScheduledTasks(qname)
|
||||||
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusNoContent)
|
writeResponseJSON(w, runAllTasksResponse{n})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRunAllRetryTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
|
func newRunAllRetryTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
qname := mux.Vars(r)["qname"]
|
qname := mux.Vars(r)["qname"]
|
||||||
if _, err := inspector.RunAllRetryTasks(qname); err != nil {
|
n, err := inspector.RunAllRetryTasks(qname)
|
||||||
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusNoContent)
|
writeResponseJSON(w, runAllTasksResponse{n})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRunAllArchivedTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
|
func newRunAllArchivedTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
qname := mux.Vars(r)["qname"]
|
qname := mux.Vars(r)["qname"]
|
||||||
if _, err := inspector.RunAllArchivedTasks(qname); err != nil {
|
n, err := inspector.RunAllArchivedTasks(qname)
|
||||||
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
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 {
|
func newArchiveAllPendingTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
qname := mux.Vars(r)["qname"]
|
qname := mux.Vars(r)["qname"]
|
||||||
if _, err := inspector.ArchiveAllPendingTasks(qname); err != nil {
|
n, err := inspector.ArchiveAllPendingTasks(qname)
|
||||||
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
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 {
|
func newArchiveAllScheduledTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
qname := mux.Vars(r)["qname"]
|
qname := mux.Vars(r)["qname"]
|
||||||
if _, err := inspector.ArchiveAllScheduledTasks(qname); err != nil {
|
n, err := inspector.ArchiveAllScheduledTasks(qname)
|
||||||
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusNoContent)
|
writeResponseJSON(w, archiveAllTasksResponse{n})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newArchiveAllRetryTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
|
func newArchiveAllRetryTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
qname := mux.Vars(r)["qname"]
|
qname := mux.Vars(r)["qname"]
|
||||||
if _, err := inspector.ArchiveAllRetryTasks(qname); err != nil {
|
n, err := inspector.ArchiveAllRetryTasks(qname)
|
||||||
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusNoContent)
|
writeResponseJSON(w, archiveAllTasksResponse{n})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -513,10 +614,7 @@ func newBatchDeleteTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc
|
|||||||
resp.DeletedIDs = append(resp.DeletedIDs, taskid)
|
resp.DeletedIDs = append(resp.DeletedIDs, taskid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
writeResponseJSON(w, resp)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -557,10 +655,7 @@ func newBatchRunTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
|
|||||||
resp.PendingIDs = append(resp.PendingIDs, taskid)
|
resp.PendingIDs = append(resp.PendingIDs, taskid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
writeResponseJSON(w, resp)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -601,10 +696,7 @@ func newBatchArchiveTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFun
|
|||||||
resp.ArchivedIDs = append(resp.ArchivedIDs, taskid)
|
resp.ArchivedIDs = append(resp.ArchivedIDs, taskid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
writeResponseJSON(w, resp)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -627,7 +719,7 @@ func getPageOptions(r *http.Request) (pageSize, pageNum int) {
|
|||||||
return pageSize, pageNum
|
return pageSize, pageNum
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGetTaskHandlerFunc(inspector *asynq.Inspector, pf PayloadFormatter) http.HandlerFunc {
|
func newGetTaskHandlerFunc(inspector *asynq.Inspector, pf PayloadFormatter, rf ResultFormatter) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
qname, taskid := vars["qname"], vars["task_id"]
|
qname, taskid := vars["qname"], vars["task_id"]
|
||||||
@ -650,9 +742,6 @@ func newGetTaskHandlerFunc(inspector *asynq.Inspector, pf PayloadFormatter) http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(toTaskInfo(info, pf)); err != nil {
|
writeResponseJSON(w, toTaskInfo(info, pf, rf))
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"main.js": "/[[.RootPath]]/static/js/main.090c4a40.chunk.js",
|
"main.js": "/[[.RootPath]]/static/js/main.5adda2da.chunk.js",
|
||||||
"main.js.map": "/[[.RootPath]]/static/js/main.090c4a40.chunk.js.map",
|
"main.js.map": "/[[.RootPath]]/static/js/main.5adda2da.chunk.js.map",
|
||||||
"runtime-main.js": "/[[.RootPath]]/static/js/runtime-main.9fea6c1a.js",
|
"runtime-main.js": "/[[.RootPath]]/static/js/runtime-main.9fea6c1a.js",
|
||||||
"runtime-main.js.map": "/[[.RootPath]]/static/js/runtime-main.9fea6c1a.js.map",
|
"runtime-main.js.map": "/[[.RootPath]]/static/js/runtime-main.9fea6c1a.js.map",
|
||||||
"static/js/2.980b0c32.chunk.js": "/[[.RootPath]]/static/js/2.980b0c32.chunk.js",
|
"static/js/2.83624df2.chunk.js": "/[[.RootPath]]/static/js/2.83624df2.chunk.js",
|
||||||
"static/js/2.980b0c32.chunk.js.map": "/[[.RootPath]]/static/js/2.980b0c32.chunk.js.map",
|
"static/js/2.83624df2.chunk.js.map": "/[[.RootPath]]/static/js/2.83624df2.chunk.js.map",
|
||||||
"index.html": "/[[.RootPath]]/index.html",
|
"index.html": "/[[.RootPath]]/index.html",
|
||||||
"static/js/2.980b0c32.chunk.js.LICENSE.txt": "/[[.RootPath]]/static/js/2.980b0c32.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": [
|
"entrypoints": [
|
||||||
"static/js/runtime-main.9fea6c1a.js",
|
"static/js/runtime-main.9fea6c1a.js",
|
||||||
"static/js/2.980b0c32.chunk.js",
|
"static/js/2.83624df2.chunk.js",
|
||||||
"static/js/main.090c4a40.chunk.js"
|
"static/js/main.5adda2da.chunk.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -1 +1 @@
|
|||||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" type="image/png" href="/[[.RootPath]]/favicon.ico"/><link rel="icon" type="image/png" sizes="32x32" href="/[[.RootPath]]/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="16x16" href="/[[.RootPath]]/favicon-16x16.png"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Asynq monitoring web console"/><link rel="apple-touch-icon" sizes="180x180" href="/[[.RootPath]]/apple-touch-icon.png"/><link rel="manifest" href="/[[.RootPath]]/manifest.json"/><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"/><script>window.ROOT_PATH=/[[.RootPath]];</script><title>Asynq - Monitoring</title></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function t(t){for(var n,i,l=t[0],a=t[1],f=t[2],c=0,s=[];c<l.length;c++)i=l[c],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n]);for(p&&p(t);s.length;)s.shift()();return u.push.apply(u,f||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,l=1;l<r.length;l++){var a=r[l];0!==o[a]&&(n=!1)}n&&(u.splice(t--,1),e=i(i.s=r[0]))}return e}var n={},o={1:0},u=[];function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.m=e,i.c=n,i.d=function(e,t,r){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(r,n,function(t){return e[t]}.bind(null,n));return r},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="/[[.RootPath]]/";var l=this.webpackJsonpui=this.webpackJsonpui||[],a=l.push.bind(l);l.push=t,l=l.slice();for(var f=0;f<l.length;f++)t(l[f]);var p=a;r()}([])</script><script src="/[[.RootPath]]/static/js/2.980b0c32.chunk.js"></script><script src="/[[.RootPath]]/static/js/main.090c4a40.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>
|
3
ui/build/static/js/2.83624df2.chunk.js
Normal file
3
ui/build/static/js/2.83624df2.chunk.js
Normal file
File diff suppressed because one or more lines are too long
1
ui/build/static/js/2.83624df2.chunk.js.map
Normal file
1
ui/build/static/js/2.83624df2.chunk.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
ui/build/static/js/main.5adda2da.chunk.js
Normal file
2
ui/build/static/js/main.5adda2da.chunk.js
Normal file
File diff suppressed because one or more lines are too long
1
ui/build/static/js/main.5adda2da.chunk.js.map
Normal file
1
ui/build/static/js/main.5adda2da.chunk.js.map
Normal file
File diff suppressed because one or more lines are too long
1
ui/build/static/media/logo-color.c2b0c1f3.svg
Normal file
1
ui/build/static/media/logo-color.c2b0c1f3.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.6 KiB |
1
ui/build/static/media/logo-white.3fa2ac55.svg
Normal file
1
ui/build/static/media/logo-white.3fa2ac55.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.6 KiB |
@ -3,37 +3,39 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material-ui/core": "4.11.3",
|
"@material-ui/core": "4.12.3",
|
||||||
"@material-ui/icons": "4.11.2",
|
"@material-ui/icons": "4.11.2",
|
||||||
"@material-ui/lab": "4.0.0-alpha.57",
|
"@material-ui/lab": "4.0.0-alpha.58",
|
||||||
"@reduxjs/toolkit": "1.5.1",
|
"@reduxjs/toolkit": "1.6.2",
|
||||||
"@testing-library/jest-dom": "^5.12.0",
|
"@testing-library/jest-dom": "^5.12.0",
|
||||||
"@testing-library/react": "^11.2.6",
|
"@testing-library/react": "^12.1.2",
|
||||||
"@testing-library/user-event": "^13.1.9",
|
"@testing-library/user-event": "^13.1.9",
|
||||||
"@types/jest": "^26.0.24",
|
"@types/jest": "^27.0.2",
|
||||||
"@types/lodash.uniqby": "4.7.6",
|
"@types/lodash.uniqby": "4.7.6",
|
||||||
"@types/node": "^16.3.1",
|
"@types/node": "^16.3.1",
|
||||||
"@types/react": "^17.0.14",
|
"@types/react": "^17.0.29",
|
||||||
"@types/react-dom": "^17.0.9",
|
"@types/react-dom": "^17.0.9",
|
||||||
"@types/react-redux": "7.1.16",
|
"@types/react-redux": "7.1.19",
|
||||||
"@types/react-router-dom": "5.1.7",
|
"@types/react-router-dom": "5.3.1",
|
||||||
"@types/react-syntax-highlighter": "13.5.2",
|
"@types/react-syntax-highlighter": "13.5.2",
|
||||||
"@types/recharts": "1.8.19",
|
"@types/recharts": "1.8.20",
|
||||||
"axios": "0.21.1",
|
"axios": "0.21.2",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
|
"dayjs": "1.10.7",
|
||||||
"lodash.uniqby": "4.7.0",
|
"lodash.uniqby": "4.7.0",
|
||||||
"query-string": "7.0.1",
|
"query-string": "7.0.1",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"react-redux": "7.2.4",
|
"react-redux": "7.2.4",
|
||||||
"react-router-dom": "5.2.0",
|
"react-router-dom": "5.3.0",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "5.0.1",
|
||||||
"react-syntax-highlighter": "15.4.3",
|
"react-syntax-highlighter": "15.4.3",
|
||||||
"recharts": "2.0.9",
|
"react-window": "1.8.6",
|
||||||
|
"recharts": "2.1.4",
|
||||||
"typescript": "~4.2.4"
|
"typescript": "~4.2.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "export PUBLIC_URL=http://localhost:3000/ && react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
@ -54,6 +56,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/react-window": "1.8.5",
|
||||||
"redux-devtools": "3.7.0"
|
"redux-devtools": "3.7.0"
|
||||||
},
|
},
|
||||||
"homepage": "/[[.RootPath]]"
|
"homepage": "/[[.RootPath]]"
|
||||||
|
@ -50,7 +50,9 @@
|
|||||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||||
/>
|
/>
|
||||||
<script>
|
<script>
|
||||||
window.ROOT_PATH=/[[.RootPath]];
|
window.FLAG_ROOT_PATH = "%PUBLIC_URL%";
|
||||||
|
window.FLAG_PROMETHEUS_SERVER_ADDRESS = "/[[.PrometheusAddr]]";
|
||||||
|
window.FLAG_READ_ONLY = "/[[.ReadOnly]]";
|
||||||
</script>
|
</script>
|
||||||
<title>Asynq - Monitoring</title>
|
<title>Asynq - Monitoring</title>
|
||||||
</head>
|
</head>
|
||||||
|
@ -12,7 +12,6 @@ import ListItemIcon from "@material-ui/core/ListItemIcon";
|
|||||||
import ListItemText from "@material-ui/core/ListItemText";
|
import ListItemText from "@material-ui/core/ListItemText";
|
||||||
import Snackbar from "@material-ui/core/Snackbar";
|
import Snackbar from "@material-ui/core/Snackbar";
|
||||||
import SnackbarContent from "@material-ui/core/SnackbarContent";
|
import SnackbarContent from "@material-ui/core/SnackbarContent";
|
||||||
import Typography from "@material-ui/core/Typography";
|
|
||||||
import IconButton from "@material-ui/core/IconButton";
|
import IconButton from "@material-ui/core/IconButton";
|
||||||
import Slide from "@material-ui/core/Slide";
|
import Slide from "@material-ui/core/Slide";
|
||||||
import { TransitionProps } from "@material-ui/core/transitions";
|
import { TransitionProps } from "@material-ui/core/transitions";
|
||||||
@ -22,10 +21,11 @@ import LayersIcon from "@material-ui/icons/Layers";
|
|||||||
import SettingsIcon from "@material-ui/icons/Settings";
|
import SettingsIcon from "@material-ui/icons/Settings";
|
||||||
import ScheduleIcon from "@material-ui/icons/Schedule";
|
import ScheduleIcon from "@material-ui/icons/Schedule";
|
||||||
import FeedbackIcon from "@material-ui/icons/Feedback";
|
import FeedbackIcon from "@material-ui/icons/Feedback";
|
||||||
|
import TimelineIcon from "@material-ui/icons/Timeline";
|
||||||
import DoubleArrowIcon from "@material-ui/icons/DoubleArrow";
|
import DoubleArrowIcon from "@material-ui/icons/DoubleArrow";
|
||||||
import CloseIcon from "@material-ui/icons/Close";
|
import CloseIcon from "@material-ui/icons/Close";
|
||||||
import { AppState } from "./store";
|
import { AppState } from "./store";
|
||||||
import { paths } from "./paths";
|
import { paths as getPaths } from "./paths";
|
||||||
import { isDarkTheme, useTheme } from "./theme";
|
import { isDarkTheme, useTheme } from "./theme";
|
||||||
import { closeSnackbar } from "./actions/snackbarActions";
|
import { closeSnackbar } from "./actions/snackbarActions";
|
||||||
import { toggleDrawer } from "./actions/settingsActions";
|
import { toggleDrawer } from "./actions/settingsActions";
|
||||||
@ -37,7 +37,10 @@ import TaskDetailsView from "./views/TaskDetailsView";
|
|||||||
import SettingsView from "./views/SettingsView";
|
import SettingsView from "./views/SettingsView";
|
||||||
import ServersView from "./views/ServersView";
|
import ServersView from "./views/ServersView";
|
||||||
import RedisInfoView from "./views/RedisInfoView";
|
import RedisInfoView from "./views/RedisInfoView";
|
||||||
|
import MetricsView from "./views/MetricsView";
|
||||||
import PageNotFoundView from "./views/PageNotFoundView";
|
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;
|
const drawerWidth = 220;
|
||||||
|
|
||||||
@ -72,9 +75,6 @@ const useStyles = (theme: Theme) =>
|
|||||||
menuButtonHidden: {
|
menuButtonHidden: {
|
||||||
display: "none",
|
display: "none",
|
||||||
},
|
},
|
||||||
title: {
|
|
||||||
flexGrow: 1,
|
|
||||||
},
|
|
||||||
drawerPaper: {
|
drawerPaper: {
|
||||||
position: "relative",
|
position: "relative",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
@ -154,6 +154,7 @@ function SlideUpTransition(props: TransitionProps) {
|
|||||||
function App(props: ConnectedProps<typeof connector>) {
|
function App(props: ConnectedProps<typeof connector>) {
|
||||||
const theme = useTheme(props.themePreference);
|
const theme = useTheme(props.themePreference);
|
||||||
const classes = useStyles(theme)();
|
const classes = useStyles(theme)();
|
||||||
|
const paths = getPaths();
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<Router>
|
<Router>
|
||||||
@ -173,15 +174,11 @@ function App(props: ConnectedProps<typeof connector>) {
|
|||||||
>
|
>
|
||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography
|
{isDarkTheme(theme) ? (
|
||||||
component="h1"
|
<LogoDarkTheme width={200} height={48} />
|
||||||
variant="h6"
|
) : (
|
||||||
noWrap
|
<Logo width={200} height={48} />
|
||||||
className={classes.title}
|
)}
|
||||||
color="textPrimary"
|
|
||||||
>
|
|
||||||
Asynq Monitoring
|
|
||||||
</Typography>
|
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
<div className={classes.mainContainer}>
|
<div className={classes.mainContainer}>
|
||||||
@ -244,6 +241,13 @@ function App(props: ConnectedProps<typeof connector>) {
|
|||||||
primary="Redis"
|
primary="Redis"
|
||||||
icon={<LayersIcon />}
|
icon={<LayersIcon />}
|
||||||
/>
|
/>
|
||||||
|
{window.PROMETHEUS_SERVER_ADDRESS && (
|
||||||
|
<ListItemLink
|
||||||
|
to={paths.QUEUE_METRICS}
|
||||||
|
primary="Metrics"
|
||||||
|
icon={<TimelineIcon />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</List>
|
</List>
|
||||||
<List>
|
<List>
|
||||||
@ -291,6 +295,9 @@ function App(props: ConnectedProps<typeof connector>) {
|
|||||||
<Route exact path={paths.HOME}>
|
<Route exact path={paths.HOME}>
|
||||||
<DashboardView />
|
<DashboardView />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route exact path={paths.QUEUE_METRICS}>
|
||||||
|
<MetricsView />
|
||||||
|
</Route>
|
||||||
<Route path="*">
|
<Route path="*">
|
||||||
<PageNotFoundView />
|
<PageNotFoundView />
|
||||||
</Route>
|
</Route>
|
||||||
|
52
ui/src/actions/groupsActions.ts
Normal file
52
ui/src/actions/groupsActions.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
48
ui/src/actions/metricsActions.ts
Normal file
48
ui/src/actions/metricsActions.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { Dispatch } from "redux";
|
||||||
|
import { getMetrics, MetricsResponse } from "../api";
|
||||||
|
import { toErrorString, toErrorStringWithHttpStatus } from "../utils";
|
||||||
|
|
||||||
|
// List of metrics related action types.
|
||||||
|
export const GET_METRICS_BEGIN = "GET_METRICS_BEGIN";
|
||||||
|
export const GET_METRICS_SUCCESS = "GET_METRICS_SUCCESS";
|
||||||
|
export const GET_METRICS_ERROR = "GET_METRICS_ERROR";
|
||||||
|
|
||||||
|
interface GetMetricsBeginAction {
|
||||||
|
type: typeof GET_METRICS_BEGIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetMetricsSuccessAction {
|
||||||
|
type: typeof GET_METRICS_SUCCESS;
|
||||||
|
payload: MetricsResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetMetricsErrorAction {
|
||||||
|
type: typeof GET_METRICS_ERROR;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Union of all metrics related actions.
|
||||||
|
export type MetricsActionTypes =
|
||||||
|
| GetMetricsBeginAction
|
||||||
|
| GetMetricsSuccessAction
|
||||||
|
| GetMetricsErrorAction;
|
||||||
|
|
||||||
|
export function getMetricsAsync(
|
||||||
|
endTime: number,
|
||||||
|
duration: number,
|
||||||
|
queues: string[]
|
||||||
|
) {
|
||||||
|
return async (dispatch: Dispatch<MetricsActionTypes>) => {
|
||||||
|
dispatch({ type: GET_METRICS_BEGIN });
|
||||||
|
try {
|
||||||
|
const response = await getMetrics(endTime, duration, queues);
|
||||||
|
dispatch({ type: GET_METRICS_SUCCESS, payload: response });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`getMetricsAsync: ${toErrorStringWithHttpStatus(error)}`);
|
||||||
|
dispatch({
|
||||||
|
type: GET_METRICS_ERROR,
|
||||||
|
error: toErrorString(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -4,6 +4,7 @@ import {
|
|||||||
batchDeleteArchivedTasks,
|
batchDeleteArchivedTasks,
|
||||||
batchDeleteRetryTasks,
|
batchDeleteRetryTasks,
|
||||||
batchDeleteScheduledTasks,
|
batchDeleteScheduledTasks,
|
||||||
|
batchDeleteCompletedTasks,
|
||||||
BatchDeleteTasksResponse,
|
BatchDeleteTasksResponse,
|
||||||
batchArchiveRetryTasks,
|
batchArchiveRetryTasks,
|
||||||
batchArchiveScheduledTasks,
|
batchArchiveScheduledTasks,
|
||||||
@ -17,23 +18,23 @@ import {
|
|||||||
deleteAllArchivedTasks,
|
deleteAllArchivedTasks,
|
||||||
deleteAllRetryTasks,
|
deleteAllRetryTasks,
|
||||||
deleteAllScheduledTasks,
|
deleteAllScheduledTasks,
|
||||||
|
deleteAllCompletedTasks,
|
||||||
deleteArchivedTask,
|
deleteArchivedTask,
|
||||||
deleteRetryTask,
|
deleteRetryTask,
|
||||||
deleteScheduledTask,
|
deleteScheduledTask,
|
||||||
|
deleteCompletedTask,
|
||||||
archiveAllRetryTasks,
|
archiveAllRetryTasks,
|
||||||
archiveAllScheduledTasks,
|
archiveAllScheduledTasks,
|
||||||
archiveRetryTask,
|
archiveRetryTask,
|
||||||
archiveScheduledTask,
|
archiveScheduledTask,
|
||||||
listActiveTasks,
|
listActiveTasks,
|
||||||
ListActiveTasksResponse,
|
|
||||||
listArchivedTasks,
|
listArchivedTasks,
|
||||||
ListArchivedTasksResponse,
|
|
||||||
listPendingTasks,
|
listPendingTasks,
|
||||||
ListPendingTasksResponse,
|
ListTasksResponse,
|
||||||
listRetryTasks,
|
listRetryTasks,
|
||||||
ListRetryTasksResponse,
|
|
||||||
listScheduledTasks,
|
listScheduledTasks,
|
||||||
ListScheduledTasksResponse,
|
listCompletedTasks,
|
||||||
|
listAggregatingTasks,
|
||||||
PaginationOptions,
|
PaginationOptions,
|
||||||
runAllArchivedTasks,
|
runAllArchivedTasks,
|
||||||
runAllRetryTasks,
|
runAllRetryTasks,
|
||||||
@ -49,6 +50,16 @@ import {
|
|||||||
archiveAllPendingTasks,
|
archiveAllPendingTasks,
|
||||||
TaskInfo,
|
TaskInfo,
|
||||||
getTaskInfo,
|
getTaskInfo,
|
||||||
|
deleteAllAggregatingTasks,
|
||||||
|
archiveAllAggregatingTasks,
|
||||||
|
runAllAggregatingTasks,
|
||||||
|
batchDeleteAggregatingTasks,
|
||||||
|
batchArchiveAggregatingTasks,
|
||||||
|
batchRunAggregatingTasks,
|
||||||
|
deleteAggregatingTask,
|
||||||
|
runAggregatingTask,
|
||||||
|
archiveAggregatingTask,
|
||||||
|
ListAggregatingTasksResponse,
|
||||||
} from "../api";
|
} from "../api";
|
||||||
import { Dispatch } from "redux";
|
import { Dispatch } from "redux";
|
||||||
import { toErrorString, toErrorStringWithHttpStatus } from "../utils";
|
import { toErrorString, toErrorStringWithHttpStatus } from "../utils";
|
||||||
@ -72,6 +83,12 @@ export const LIST_RETRY_TASKS_ERROR = "LIST_RETRY_TASKS_ERROR";
|
|||||||
export const LIST_ARCHIVED_TASKS_BEGIN = "LIST_ARCHIVED_TASKS_BEGIN";
|
export const LIST_ARCHIVED_TASKS_BEGIN = "LIST_ARCHIVED_TASKS_BEGIN";
|
||||||
export const LIST_ARCHIVED_TASKS_SUCCESS = "LIST_ARCHIVED_TASKS_SUCCESS";
|
export const LIST_ARCHIVED_TASKS_SUCCESS = "LIST_ARCHIVED_TASKS_SUCCESS";
|
||||||
export const LIST_ARCHIVED_TASKS_ERROR = "LIST_ARCHIVED_TASKS_ERROR";
|
export const LIST_ARCHIVED_TASKS_ERROR = "LIST_ARCHIVED_TASKS_ERROR";
|
||||||
|
export const LIST_COMPLETED_TASKS_BEGIN = "LIST_COMPLETED_TASKS_BEGIN";
|
||||||
|
export const LIST_COMPLETED_TASKS_SUCCESS = "LIST_COMPLETED_TASKS_SUCCESS";
|
||||||
|
export const LIST_COMPLETED_TASKS_ERROR = "LIST_COMPLETED_TASKS_ERROR";
|
||||||
|
export const 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_BEGIN = "CANCEL_ACTIVE_TASK_BEGIN";
|
||||||
export const CANCEL_ACTIVE_TASK_SUCCESS = "CANCEL_ACTIVE_TASK_SUCCESS";
|
export const CANCEL_ACTIVE_TASK_SUCCESS = "CANCEL_ACTIVE_TASK_SUCCESS";
|
||||||
export const CANCEL_ACTIVE_TASK_ERROR = "CANCEL_ACTIVE_TASK_ERROR";
|
export const CANCEL_ACTIVE_TASK_ERROR = "CANCEL_ACTIVE_TASK_ERROR";
|
||||||
@ -109,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_BEGIN = "ARCHIVE_RETRY_TASK_BEGIN";
|
||||||
export const ARCHIVE_RETRY_TASK_SUCCESS = "ARCHIVE_RETRY_TASK_SUCCESS";
|
export const ARCHIVE_RETRY_TASK_SUCCESS = "ARCHIVE_RETRY_TASK_SUCCESS";
|
||||||
export const ARCHIVE_RETRY_TASK_ERROR = "ARCHIVE_RETRY_TASK_ERROR";
|
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 =
|
export const BATCH_ARCHIVE_PENDING_TASKS_BEGIN =
|
||||||
"BATCH_ARCHIVE_PENDING_TASKS_BEGIN";
|
"BATCH_ARCHIVE_PENDING_TASKS_BEGIN";
|
||||||
export const BATCH_ARCHIVE_PENDING_TASKS_SUCCESS =
|
export const BATCH_ARCHIVE_PENDING_TASKS_SUCCESS =
|
||||||
@ -213,6 +241,57 @@ export const DELETE_ALL_ARCHIVED_TASKS_SUCCESS =
|
|||||||
"DELETE_ALL_ARCHIVED_TASKS_SUCCESS";
|
"DELETE_ALL_ARCHIVED_TASKS_SUCCESS";
|
||||||
export const DELETE_ALL_ARCHIVED_TASKS_ERROR =
|
export const DELETE_ALL_ARCHIVED_TASKS_ERROR =
|
||||||
"DELETE_ALL_ARCHIVED_TASKS_ERROR";
|
"DELETE_ALL_ARCHIVED_TASKS_ERROR";
|
||||||
|
export const DELETE_COMPLETED_TASK_BEGIN = "DELETE_COMPLETED_TASK_BEGIN";
|
||||||
|
export const DELETE_COMPLETED_TASK_SUCCESS = "DELETE_COMPLETED_TASK_SUCCESS";
|
||||||
|
export const DELETE_COMPLETED_TASK_ERROR = "DELETE_COMPLETED_TASK_ERROR";
|
||||||
|
export const DELETE_ALL_COMPLETED_TASKS_BEGIN =
|
||||||
|
"DELETE_ALL_COMPLETED_TASKS_BEGIN";
|
||||||
|
export const DELETE_ALL_COMPLETED_TASKS_SUCCESS =
|
||||||
|
"DELETE_ALL_COMPLETED_TASKS_SUCCESS";
|
||||||
|
export const DELETE_ALL_COMPLETED_TASKS_ERROR =
|
||||||
|
"DELETE_ALL_COMPLETED_TASKS_ERROR";
|
||||||
|
export const BATCH_DELETE_COMPLETED_TASKS_BEGIN =
|
||||||
|
"BATCH_DELETE_COMPLETED_TASKS_BEGIN";
|
||||||
|
export const BATCH_DELETE_COMPLETED_TASKS_SUCCESS =
|
||||||
|
"BATCH_DELETE_COMPLETED_TASKS_SUCCESS";
|
||||||
|
export const BATCH_DELETE_COMPLETED_TASKS_ERROR =
|
||||||
|
"BATCH_DELETE_COMPLETED_TASKS_ERROR";
|
||||||
|
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 {
|
interface GetTaskInfoBeginAction {
|
||||||
type: typeof GET_TASK_INFO_BEGIN;
|
type: typeof GET_TASK_INFO_BEGIN;
|
||||||
@ -236,7 +315,7 @@ interface ListActiveTasksBeginAction {
|
|||||||
interface ListActiveTasksSuccessAction {
|
interface ListActiveTasksSuccessAction {
|
||||||
type: typeof LIST_ACTIVE_TASKS_SUCCESS;
|
type: typeof LIST_ACTIVE_TASKS_SUCCESS;
|
||||||
queue: string;
|
queue: string;
|
||||||
payload: ListActiveTasksResponse;
|
payload: ListTasksResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ListActiveTasksErrorAction {
|
interface ListActiveTasksErrorAction {
|
||||||
@ -253,7 +332,7 @@ interface ListPendingTasksBeginAction {
|
|||||||
interface ListPendingTasksSuccessAction {
|
interface ListPendingTasksSuccessAction {
|
||||||
type: typeof LIST_PENDING_TASKS_SUCCESS;
|
type: typeof LIST_PENDING_TASKS_SUCCESS;
|
||||||
queue: string;
|
queue: string;
|
||||||
payload: ListPendingTasksResponse;
|
payload: ListTasksResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ListPendingTasksErrorAction {
|
interface ListPendingTasksErrorAction {
|
||||||
@ -270,7 +349,7 @@ interface ListScheduledTasksBeginAction {
|
|||||||
interface ListScheduledTasksSuccessAction {
|
interface ListScheduledTasksSuccessAction {
|
||||||
type: typeof LIST_SCHEDULED_TASKS_SUCCESS;
|
type: typeof LIST_SCHEDULED_TASKS_SUCCESS;
|
||||||
queue: string;
|
queue: string;
|
||||||
payload: ListScheduledTasksResponse;
|
payload: ListTasksResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ListScheduledTasksErrorAction {
|
interface ListScheduledTasksErrorAction {
|
||||||
@ -287,7 +366,7 @@ interface ListRetryTasksBeginAction {
|
|||||||
interface ListRetryTasksSuccessAction {
|
interface ListRetryTasksSuccessAction {
|
||||||
type: typeof LIST_RETRY_TASKS_SUCCESS;
|
type: typeof LIST_RETRY_TASKS_SUCCESS;
|
||||||
queue: string;
|
queue: string;
|
||||||
payload: ListRetryTasksResponse;
|
payload: ListTasksResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ListRetryTasksErrorAction {
|
interface ListRetryTasksErrorAction {
|
||||||
@ -304,7 +383,7 @@ interface ListArchivedTasksBeginAction {
|
|||||||
interface ListArchivedTasksSuccessAction {
|
interface ListArchivedTasksSuccessAction {
|
||||||
type: typeof LIST_ARCHIVED_TASKS_SUCCESS;
|
type: typeof LIST_ARCHIVED_TASKS_SUCCESS;
|
||||||
queue: string;
|
queue: string;
|
||||||
payload: ListArchivedTasksResponse;
|
payload: ListTasksResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ListArchivedTasksErrorAction {
|
interface ListArchivedTasksErrorAction {
|
||||||
@ -313,6 +392,43 @@ interface ListArchivedTasksErrorAction {
|
|||||||
error: string; // error description
|
error: string; // error description
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ListCompletedTasksBeginAction {
|
||||||
|
type: typeof LIST_COMPLETED_TASKS_BEGIN;
|
||||||
|
queue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListCompletedTasksSuccessAction {
|
||||||
|
type: typeof LIST_COMPLETED_TASKS_SUCCESS;
|
||||||
|
queue: string;
|
||||||
|
payload: ListTasksResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListCompletedTasksErrorAction {
|
||||||
|
type: typeof LIST_COMPLETED_TASKS_ERROR;
|
||||||
|
queue: string;
|
||||||
|
error: string; // error description
|
||||||
|
}
|
||||||
|
|
||||||
|
interface 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 {
|
interface CancelActiveTaskBeginAction {
|
||||||
type: typeof CANCEL_ACTIVE_TASK_BEGIN;
|
type: typeof CANCEL_ACTIVE_TASK_BEGIN;
|
||||||
queue: string;
|
queue: string;
|
||||||
@ -911,6 +1027,244 @@ interface DeleteAllArchivedTasksErrorAction {
|
|||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DeleteCompletedTaskBeginAction {
|
||||||
|
type: typeof DELETE_COMPLETED_TASK_BEGIN;
|
||||||
|
queue: string;
|
||||||
|
taskId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteCompletedTaskSuccessAction {
|
||||||
|
type: typeof DELETE_COMPLETED_TASK_SUCCESS;
|
||||||
|
queue: string;
|
||||||
|
taskId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteCompletedTaskErrorAction {
|
||||||
|
type: typeof DELETE_COMPLETED_TASK_ERROR;
|
||||||
|
queue: string;
|
||||||
|
taskId: string;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BatchDeleteCompletedTasksBeginAction {
|
||||||
|
type: typeof BATCH_DELETE_COMPLETED_TASKS_BEGIN;
|
||||||
|
queue: string;
|
||||||
|
taskIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BatchDeleteCompletedTasksSuccessAction {
|
||||||
|
type: typeof BATCH_DELETE_COMPLETED_TASKS_SUCCESS;
|
||||||
|
queue: string;
|
||||||
|
payload: BatchDeleteTasksResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BatchDeleteCompletedTasksErrorAction {
|
||||||
|
type: typeof BATCH_DELETE_COMPLETED_TASKS_ERROR;
|
||||||
|
queue: string;
|
||||||
|
taskIds: string[];
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteAllCompletedTasksBeginAction {
|
||||||
|
type: typeof DELETE_ALL_COMPLETED_TASKS_BEGIN;
|
||||||
|
queue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteAllCompletedTasksSuccessAction {
|
||||||
|
type: typeof DELETE_ALL_COMPLETED_TASKS_SUCCESS;
|
||||||
|
queue: string;
|
||||||
|
deleted: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteAllCompletedTasksErrorAction {
|
||||||
|
type: typeof DELETE_ALL_COMPLETED_TASKS_ERROR;
|
||||||
|
queue: string;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
// Union of all tasks related action types.
|
||||||
export type TasksActionTypes =
|
export type TasksActionTypes =
|
||||||
| GetTaskInfoBeginAction
|
| GetTaskInfoBeginAction
|
||||||
@ -931,6 +1285,12 @@ export type TasksActionTypes =
|
|||||||
| ListArchivedTasksBeginAction
|
| ListArchivedTasksBeginAction
|
||||||
| ListArchivedTasksSuccessAction
|
| ListArchivedTasksSuccessAction
|
||||||
| ListArchivedTasksErrorAction
|
| ListArchivedTasksErrorAction
|
||||||
|
| ListCompletedTasksBeginAction
|
||||||
|
| ListCompletedTasksSuccessAction
|
||||||
|
| ListCompletedTasksErrorAction
|
||||||
|
| ListAggregatingTasksBeginAction
|
||||||
|
| ListAggregatingTasksSuccessAction
|
||||||
|
| ListAggregatingTasksErrorAction
|
||||||
| CancelActiveTaskBeginAction
|
| CancelActiveTaskBeginAction
|
||||||
| CancelActiveTaskSuccessAction
|
| CancelActiveTaskSuccessAction
|
||||||
| CancelActiveTaskErrorAction
|
| CancelActiveTaskErrorAction
|
||||||
@ -1029,7 +1389,43 @@ export type TasksActionTypes =
|
|||||||
| RunAllArchivedTasksErrorAction
|
| RunAllArchivedTasksErrorAction
|
||||||
| DeleteAllArchivedTasksBeginAction
|
| DeleteAllArchivedTasksBeginAction
|
||||||
| DeleteAllArchivedTasksSuccessAction
|
| DeleteAllArchivedTasksSuccessAction
|
||||||
| DeleteAllArchivedTasksErrorAction;
|
| DeleteAllArchivedTasksErrorAction
|
||||||
|
| DeleteCompletedTaskBeginAction
|
||||||
|
| DeleteCompletedTaskSuccessAction
|
||||||
|
| DeleteCompletedTaskErrorAction
|
||||||
|
| BatchDeleteCompletedTasksBeginAction
|
||||||
|
| BatchDeleteCompletedTasksSuccessAction
|
||||||
|
| BatchDeleteCompletedTasksErrorAction
|
||||||
|
| DeleteAllCompletedTasksBeginAction
|
||||||
|
| DeleteAllCompletedTasksSuccessAction
|
||||||
|
| 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) {
|
export function getTaskInfoAsync(qname: string, id: string) {
|
||||||
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
||||||
@ -1039,15 +1435,15 @@ export function getTaskInfoAsync(qname: string, id: string) {
|
|||||||
dispatch({
|
dispatch({
|
||||||
type: GET_TASK_INFO_SUCCESS,
|
type: GET_TASK_INFO_SUCCESS,
|
||||||
payload: response,
|
payload: response,
|
||||||
})
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("getTaskInfoAsync: ", toErrorStringWithHttpStatus(error));
|
console.error("getTaskInfoAsync: ", toErrorStringWithHttpStatus(error));
|
||||||
dispatch({
|
dispatch({
|
||||||
type: GET_TASK_INFO_ERROR,
|
type: GET_TASK_INFO_ERROR,
|
||||||
error: toErrorString(error),
|
error: toErrorString(error),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listActiveTasksAsync(
|
export function listActiveTasksAsync(
|
||||||
@ -1185,6 +1581,67 @@ export function listArchivedTasksAsync(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listCompletedTasksAsync(
|
||||||
|
qname: string,
|
||||||
|
pageOpts?: PaginationOptions
|
||||||
|
) {
|
||||||
|
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
||||||
|
try {
|
||||||
|
dispatch({ type: LIST_COMPLETED_TASKS_BEGIN, queue: qname });
|
||||||
|
const response = await listCompletedTasks(qname, pageOpts);
|
||||||
|
dispatch({
|
||||||
|
type: LIST_COMPLETED_TASKS_SUCCESS,
|
||||||
|
queue: qname,
|
||||||
|
payload: response,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"listCompletedTasksAsync: ",
|
||||||
|
toErrorStringWithHttpStatus(error)
|
||||||
|
);
|
||||||
|
dispatch({
|
||||||
|
type: LIST_COMPLETED_TASKS_ERROR,
|
||||||
|
queue: qname,
|
||||||
|
error: toErrorString(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function 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) {
|
export function cancelActiveTaskAsync(queue: string, taskId: string) {
|
||||||
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
||||||
dispatch({ type: CANCEL_ACTIVE_TASK_BEGIN, queue, taskId });
|
dispatch({ type: CANCEL_ACTIVE_TASK_BEGIN, queue, taskId });
|
||||||
@ -1395,10 +1852,7 @@ export function deletePendingTaskAsync(queue: string, taskId: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function batchDeletePendingTasksAsync(
|
export function batchDeletePendingTasksAsync(queue: string, taskIds: string[]) {
|
||||||
queue: string,
|
|
||||||
taskIds: string[]
|
|
||||||
) {
|
|
||||||
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
||||||
dispatch({ type: BATCH_DELETE_PENDING_TASKS_BEGIN, queue, taskIds });
|
dispatch({ type: BATCH_DELETE_PENDING_TASKS_BEGIN, queue, taskIds });
|
||||||
try {
|
try {
|
||||||
@ -1553,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) {
|
export function archiveAllPendingTasksAsync(queue: string) {
|
||||||
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
||||||
dispatch({ type: ARCHIVE_ALL_PENDING_TASKS_BEGIN, queue });
|
dispatch({ type: ARCHIVE_ALL_PENDING_TASKS_BEGIN, queue });
|
||||||
@ -1597,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) {
|
export function deleteAllScheduledTasksAsync(queue: string) {
|
||||||
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
||||||
dispatch({ type: DELETE_ALL_SCHEDULED_TASKS_BEGIN, queue });
|
dispatch({ type: DELETE_ALL_SCHEDULED_TASKS_BEGIN, queue });
|
||||||
@ -1661,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) {
|
export function deleteRetryTaskAsync(queue: string, taskId: string) {
|
||||||
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
||||||
dispatch({ type: DELETE_RETRY_TASK_BEGIN, queue, taskId });
|
dispatch({ type: DELETE_RETRY_TASK_BEGIN, queue, taskId });
|
||||||
@ -1732,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[]) {
|
export function batchArchiveRetryTasksAsync(queue: string, taskIds: string[]) {
|
||||||
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
||||||
dispatch({ type: BATCH_ARCHIVE_RETRY_TASKS_BEGIN, queue, taskIds });
|
dispatch({ type: BATCH_ARCHIVE_RETRY_TASKS_BEGIN, queue, taskIds });
|
||||||
@ -1938,3 +2621,112 @@ export function runAllArchivedTasksAsync(queue: string) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function deleteCompletedTaskAsync(queue: string, taskId: string) {
|
||||||
|
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
||||||
|
dispatch({ type: DELETE_COMPLETED_TASK_BEGIN, queue, taskId });
|
||||||
|
try {
|
||||||
|
await deleteCompletedTask(queue, taskId);
|
||||||
|
dispatch({ type: DELETE_COMPLETED_TASK_SUCCESS, queue, taskId });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"deleteCompletedTaskAsync: ",
|
||||||
|
toErrorStringWithHttpStatus(error)
|
||||||
|
);
|
||||||
|
dispatch({
|
||||||
|
type: DELETE_COMPLETED_TASK_ERROR,
|
||||||
|
error: toErrorString(error),
|
||||||
|
queue,
|
||||||
|
taskId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function batchDeleteCompletedTasksAsync(
|
||||||
|
queue: string,
|
||||||
|
taskIds: string[]
|
||||||
|
) {
|
||||||
|
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
||||||
|
dispatch({ type: BATCH_DELETE_COMPLETED_TASKS_BEGIN, queue, taskIds });
|
||||||
|
try {
|
||||||
|
const response = await batchDeleteCompletedTasks(queue, taskIds);
|
||||||
|
dispatch({
|
||||||
|
type: BATCH_DELETE_COMPLETED_TASKS_SUCCESS,
|
||||||
|
queue: queue,
|
||||||
|
payload: response,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"batchDeleteCompletedTasksAsync: ",
|
||||||
|
toErrorStringWithHttpStatus(error)
|
||||||
|
);
|
||||||
|
dispatch({
|
||||||
|
type: BATCH_DELETE_COMPLETED_TASKS_ERROR,
|
||||||
|
error: toErrorString(error),
|
||||||
|
queue,
|
||||||
|
taskIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function 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 });
|
||||||
|
try {
|
||||||
|
const response = await deleteAllCompletedTasks(queue);
|
||||||
|
dispatch({
|
||||||
|
type: DELETE_ALL_COMPLETED_TASKS_SUCCESS,
|
||||||
|
deleted: response.deleted,
|
||||||
|
queue,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"deleteAllCompletedTasksAsync: ",
|
||||||
|
toErrorStringWithHttpStatus(error)
|
||||||
|
);
|
||||||
|
dispatch({
|
||||||
|
type: DELETE_ALL_COMPLETED_TASKS_ERROR,
|
||||||
|
error: toErrorString(error),
|
||||||
|
queue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
469
ui/src/api.ts
469
ui/src/api.ts
@ -4,36 +4,24 @@ import queryString from "query-string";
|
|||||||
// In production build, API server is on listening on the same port as
|
// In production build, API server is on listening on the same port as
|
||||||
// the static file server.
|
// the static file server.
|
||||||
// In developement, we assume that the API server is listening on port 8080.
|
// In developement, we assume that the API server is listening on port 8080.
|
||||||
const BASE_URL =
|
const getBaseUrl = () =>
|
||||||
process.env.NODE_ENV === "production" ? `${window.ROOT_PATH}/api` : `http://localhost:8080${window.ROOT_PATH}/api`;
|
process.env.NODE_ENV === "production"
|
||||||
|
? `${window.ROOT_PATH}/api`
|
||||||
|
: `http://localhost:8080${window.ROOT_PATH}/api`;
|
||||||
|
|
||||||
export interface ListQueuesResponse {
|
export interface ListQueuesResponse {
|
||||||
queues: Queue[];
|
queues: Queue[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListActiveTasksResponse {
|
export interface ListTasksResponse {
|
||||||
tasks: ActiveTask[];
|
tasks: TaskInfo[];
|
||||||
stats: Queue;
|
stats: Queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListPendingTasksResponse {
|
export interface ListAggregatingTasksResponse {
|
||||||
tasks: PendingTask[];
|
tasks: TaskInfo[];
|
||||||
stats: Queue;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ListScheduledTasksResponse {
|
|
||||||
tasks: ScheduledTask[];
|
|
||||||
stats: Queue;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ListRetryTasksResponse {
|
|
||||||
tasks: RetryTask[];
|
|
||||||
stats: Queue;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ListArchivedTasksResponse {
|
|
||||||
tasks: ArchivedTask[];
|
|
||||||
stats: Queue;
|
stats: Queue;
|
||||||
|
groups: GroupInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListServersResponse {
|
export interface ListServersResponse {
|
||||||
@ -72,10 +60,23 @@ export interface DeleteAllTasksResponse {
|
|||||||
deleted: number;
|
deleted: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ArchiveAllTasksResponse {
|
||||||
|
archived: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunAllTasksResponse {
|
||||||
|
scheduled: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ListQueueStatsResponse {
|
export interface ListQueueStatsResponse {
|
||||||
stats: { [qname: string]: DailyStat[] };
|
stats: { [qname: string]: DailyStat[] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ListGroupsResponse {
|
||||||
|
stats: Queue;
|
||||||
|
groups: GroupInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface RedisInfoResponse {
|
export interface RedisInfoResponse {
|
||||||
address: string;
|
address: string;
|
||||||
info: RedisInfo;
|
info: RedisInfo;
|
||||||
@ -94,6 +95,45 @@ export interface QueueLocation {
|
|||||||
nodes: string[]; // node addresses
|
nodes: string[]; // node addresses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MetricsResponse {
|
||||||
|
queue_size: PrometheusMetricsResponse;
|
||||||
|
queue_latency_seconds: PrometheusMetricsResponse;
|
||||||
|
queue_memory_usage_approx_bytes: PrometheusMetricsResponse;
|
||||||
|
tasks_processed_per_second: PrometheusMetricsResponse;
|
||||||
|
tasks_failed_per_second: PrometheusMetricsResponse;
|
||||||
|
error_rate: PrometheusMetricsResponse;
|
||||||
|
pending_tasks_by_queue: PrometheusMetricsResponse;
|
||||||
|
retry_tasks_by_queue: PrometheusMetricsResponse;
|
||||||
|
archived_tasks_by_queue: PrometheusMetricsResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrometheusMetricsResponse {
|
||||||
|
status: "success" | "error";
|
||||||
|
data?: MetricsResult; // present if status === "success"
|
||||||
|
error?: string; // present if status === "error"
|
||||||
|
errorType?: string; // present if status === "error"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricsResult {
|
||||||
|
resultType: string;
|
||||||
|
result: Metrics[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Metrics {
|
||||||
|
metric: MetricsInfo;
|
||||||
|
values: [number, string][]; // [unixtime, value]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricsInfo {
|
||||||
|
__name__: string;
|
||||||
|
instance: string;
|
||||||
|
job: string;
|
||||||
|
|
||||||
|
// labels (may or may not be present depending on metrics)
|
||||||
|
queue?: string;
|
||||||
|
state?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Return value from redis INFO command.
|
// Return value from redis INFO command.
|
||||||
// See https://redis.io/commands/info#return-value.
|
// See https://redis.io/commands/info#return-value.
|
||||||
export interface RedisInfo {
|
export interface RedisInfo {
|
||||||
@ -229,16 +269,26 @@ export interface RedisInfo {
|
|||||||
used_memory_startup: string;
|
used_memory_startup: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GroupInfo {
|
||||||
|
group: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Queue {
|
export interface Queue {
|
||||||
queue: string;
|
queue: string;
|
||||||
paused: boolean;
|
paused: boolean;
|
||||||
size: number;
|
size: number;
|
||||||
|
groups: number;
|
||||||
|
latency_msec: number;
|
||||||
|
display_latency: string;
|
||||||
memory_usage_bytes: number;
|
memory_usage_bytes: number;
|
||||||
active: number;
|
active: number;
|
||||||
pending: number;
|
pending: number;
|
||||||
|
aggregating: number;
|
||||||
scheduled: number;
|
scheduled: number;
|
||||||
retry: number;
|
retry: number;
|
||||||
archived: number;
|
archived: number;
|
||||||
|
completed: number;
|
||||||
processed: number;
|
processed: number;
|
||||||
failed: number;
|
failed: number;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
@ -251,18 +301,13 @@ export interface DailyStat {
|
|||||||
failed: number;
|
failed: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// BaseTask corresponds to asynq.Task type.
|
|
||||||
interface BaseTask {
|
|
||||||
type: string;
|
|
||||||
payload: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TaskInfo {
|
export interface TaskInfo {
|
||||||
id: string;
|
id: string;
|
||||||
queue: string;
|
queue: string;
|
||||||
type: string;
|
type: string;
|
||||||
payload: string;
|
payload: string;
|
||||||
state: string;
|
state: string;
|
||||||
|
start_time: string; // Only applies to task.state == 'active'
|
||||||
max_retry: number;
|
max_retry: number;
|
||||||
retried: number;
|
retried: number;
|
||||||
last_failed_at: string;
|
last_failed_at: string;
|
||||||
@ -270,51 +315,11 @@ export interface TaskInfo {
|
|||||||
next_process_at: string;
|
next_process_at: string;
|
||||||
timeout_seconds: number;
|
timeout_seconds: number;
|
||||||
deadline: string;
|
deadline: string;
|
||||||
}
|
group: string;
|
||||||
|
completed_at: string;
|
||||||
export interface ActiveTask extends BaseTask {
|
result: string;
|
||||||
id: string;
|
ttl_seconds: number;
|
||||||
queue: string;
|
is_orphaned: boolean; // Only applies to task.state == 'active'
|
||||||
start_time: string;
|
|
||||||
deadline: string;
|
|
||||||
max_retry: number;
|
|
||||||
retried: number;
|
|
||||||
error_message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PendingTask extends BaseTask {
|
|
||||||
id: string;
|
|
||||||
queue: string;
|
|
||||||
max_retry: number;
|
|
||||||
retried: number;
|
|
||||||
error_message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ScheduledTask extends BaseTask {
|
|
||||||
id: string;
|
|
||||||
queue: string;
|
|
||||||
max_retry: number;
|
|
||||||
retried: number;
|
|
||||||
error_message: string;
|
|
||||||
next_process_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RetryTask extends BaseTask {
|
|
||||||
id: string;
|
|
||||||
queue: string;
|
|
||||||
next_process_at: string;
|
|
||||||
max_retry: number;
|
|
||||||
retried: number;
|
|
||||||
error_message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ArchivedTask extends BaseTask {
|
|
||||||
id: string;
|
|
||||||
queue: string;
|
|
||||||
max_retry: number;
|
|
||||||
retried: number;
|
|
||||||
last_failed_at: string;
|
|
||||||
error_message: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServerInfo {
|
export interface ServerInfo {
|
||||||
@ -362,7 +367,7 @@ export interface PaginationOptions extends Record<string, number | undefined> {
|
|||||||
export async function listQueues(): Promise<ListQueuesResponse> {
|
export async function listQueues(): Promise<ListQueuesResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "get",
|
method: "get",
|
||||||
url: `${BASE_URL}/queues`,
|
url: `${getBaseUrl()}/queues`,
|
||||||
});
|
});
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
@ -370,34 +375,45 @@ export async function listQueues(): Promise<ListQueuesResponse> {
|
|||||||
export async function deleteQueue(qname: string): Promise<void> {
|
export async function deleteQueue(qname: string): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "delete",
|
method: "delete",
|
||||||
url: `${BASE_URL}/queues/${qname}`,
|
url: `${getBaseUrl()}/queues/${qname}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pauseQueue(qname: string): Promise<void> {
|
export async function pauseQueue(qname: string): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}:pause`,
|
url: `${getBaseUrl()}/queues/${qname}:pause`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resumeQueue(qname: string): Promise<void> {
|
export async function resumeQueue(qname: string): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}:resume`,
|
url: `${getBaseUrl()}/queues/${qname}:resume`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listQueueStats(): Promise<ListQueueStatsResponse> {
|
export async function listQueueStats(): Promise<ListQueueStatsResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "get",
|
method: "get",
|
||||||
url: `${BASE_URL}/queue_stats`,
|
url: `${getBaseUrl()}/queue_stats`,
|
||||||
});
|
});
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTaskInfo(qname: string, id: string): Promise<TaskInfo> {
|
export async function listGroups(qname: string): Promise<ListGroupsResponse> {
|
||||||
const url = `${BASE_URL}/queues/${qname}/tasks/${id}`;
|
const resp = await axios({
|
||||||
|
method: "get",
|
||||||
|
url: `${getBaseUrl()}/queues/${qname}/groups`,
|
||||||
|
});
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTaskInfo(
|
||||||
|
qname: string,
|
||||||
|
id: string
|
||||||
|
): Promise<TaskInfo> {
|
||||||
|
const url = `${getBaseUrl()}/queues/${qname}/tasks/${id}`;
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "get",
|
method: "get",
|
||||||
url,
|
url,
|
||||||
@ -408,8 +424,8 @@ export async function getTaskInfo(qname: string, id: string): Promise<TaskInfo>
|
|||||||
export async function listActiveTasks(
|
export async function listActiveTasks(
|
||||||
qname: string,
|
qname: string,
|
||||||
pageOpts?: PaginationOptions
|
pageOpts?: PaginationOptions
|
||||||
): Promise<ListActiveTasksResponse> {
|
): Promise<ListTasksResponse> {
|
||||||
let url = `${BASE_URL}/queues/${qname}/active_tasks`;
|
let url = `${getBaseUrl()}/queues/${qname}/active_tasks`;
|
||||||
if (pageOpts) {
|
if (pageOpts) {
|
||||||
url += `?${queryString.stringify(pageOpts)}`;
|
url += `?${queryString.stringify(pageOpts)}`;
|
||||||
}
|
}
|
||||||
@ -426,14 +442,14 @@ export async function cancelActiveTask(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "post",
|
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> {
|
export async function cancelAllActiveTasks(qname: string): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/active_tasks:cancel_all`,
|
url: `${getBaseUrl()}/queues/${qname}/active_tasks:cancel_all`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -443,7 +459,7 @@ export async function batchCancelActiveTasks(
|
|||||||
): Promise<BatchCancelTasksResponse> {
|
): Promise<BatchCancelTasksResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/active_tasks:batch_cancel`,
|
url: `${getBaseUrl()}/queues/${qname}/active_tasks:batch_cancel`,
|
||||||
data: {
|
data: {
|
||||||
task_ids: taskIds,
|
task_ids: taskIds,
|
||||||
},
|
},
|
||||||
@ -454,8 +470,8 @@ export async function batchCancelActiveTasks(
|
|||||||
export async function listPendingTasks(
|
export async function listPendingTasks(
|
||||||
qname: string,
|
qname: string,
|
||||||
pageOpts?: PaginationOptions
|
pageOpts?: PaginationOptions
|
||||||
): Promise<ListPendingTasksResponse> {
|
): Promise<ListTasksResponse> {
|
||||||
let url = `${BASE_URL}/queues/${qname}/pending_tasks`;
|
let url = `${getBaseUrl()}/queues/${qname}/pending_tasks`;
|
||||||
if (pageOpts) {
|
if (pageOpts) {
|
||||||
url += `?${queryString.stringify(pageOpts)}`;
|
url += `?${queryString.stringify(pageOpts)}`;
|
||||||
}
|
}
|
||||||
@ -469,8 +485,8 @@ export async function listPendingTasks(
|
|||||||
export async function listScheduledTasks(
|
export async function listScheduledTasks(
|
||||||
qname: string,
|
qname: string,
|
||||||
pageOpts?: PaginationOptions
|
pageOpts?: PaginationOptions
|
||||||
): Promise<ListScheduledTasksResponse> {
|
): Promise<ListTasksResponse> {
|
||||||
let url = `${BASE_URL}/queues/${qname}/scheduled_tasks`;
|
let url = `${getBaseUrl()}/queues/${qname}/scheduled_tasks`;
|
||||||
if (pageOpts) {
|
if (pageOpts) {
|
||||||
url += `?${queryString.stringify(pageOpts)}`;
|
url += `?${queryString.stringify(pageOpts)}`;
|
||||||
}
|
}
|
||||||
@ -484,8 +500,8 @@ export async function listScheduledTasks(
|
|||||||
export async function listRetryTasks(
|
export async function listRetryTasks(
|
||||||
qname: string,
|
qname: string,
|
||||||
pageOpts?: PaginationOptions
|
pageOpts?: PaginationOptions
|
||||||
): Promise<ListRetryTasksResponse> {
|
): Promise<ListTasksResponse> {
|
||||||
let url = `${BASE_URL}/queues/${qname}/retry_tasks`;
|
let url = `${getBaseUrl()}/queues/${qname}/retry_tasks`;
|
||||||
if (pageOpts) {
|
if (pageOpts) {
|
||||||
url += `?${queryString.stringify(pageOpts)}`;
|
url += `?${queryString.stringify(pageOpts)}`;
|
||||||
}
|
}
|
||||||
@ -499,8 +515,39 @@ export async function listRetryTasks(
|
|||||||
export async function listArchivedTasks(
|
export async function listArchivedTasks(
|
||||||
qname: string,
|
qname: string,
|
||||||
pageOpts?: PaginationOptions
|
pageOpts?: PaginationOptions
|
||||||
): Promise<ListArchivedTasksResponse> {
|
): Promise<ListTasksResponse> {
|
||||||
let url = `${BASE_URL}/queues/${qname}/archived_tasks`;
|
let url = `${getBaseUrl()}/queues/${qname}/archived_tasks`;
|
||||||
|
if (pageOpts) {
|
||||||
|
url += `?${queryString.stringify(pageOpts)}`;
|
||||||
|
}
|
||||||
|
const resp = await axios({
|
||||||
|
method: "get",
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listCompletedTasks(
|
||||||
|
qname: string,
|
||||||
|
pageOpts?: PaginationOptions
|
||||||
|
): Promise<ListTasksResponse> {
|
||||||
|
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) {
|
if (pageOpts) {
|
||||||
url += `?${queryString.stringify(pageOpts)}`;
|
url += `?${queryString.stringify(pageOpts)}`;
|
||||||
}
|
}
|
||||||
@ -517,7 +564,7 @@ export async function archivePendingTask(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/pending_tasks/${taskId}:archive`,
|
url: `${getBaseUrl()}/queues/${qname}/pending_tasks/${taskId}:archive`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -527,7 +574,7 @@ export async function batchArchivePendingTasks(
|
|||||||
): Promise<BatchArchiveTasksResponse> {
|
): Promise<BatchArchiveTasksResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/pending_tasks:batch_archive`,
|
url: `${getBaseUrl()}/queues/${qname}/pending_tasks:batch_archive`,
|
||||||
data: {
|
data: {
|
||||||
task_ids: taskIds,
|
task_ids: taskIds,
|
||||||
},
|
},
|
||||||
@ -538,7 +585,7 @@ export async function batchArchivePendingTasks(
|
|||||||
export async function archiveAllPendingTasks(qname: string): Promise<void> {
|
export async function archiveAllPendingTasks(qname: string): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/pending_tasks:archive_all`,
|
url: `${getBaseUrl()}/queues/${qname}/pending_tasks:archive_all`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -548,7 +595,7 @@ export async function deletePendingTask(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "delete",
|
method: "delete",
|
||||||
url: `${BASE_URL}/queues/${qname}/pending_tasks/${taskId}`,
|
url: `${getBaseUrl()}/queues/${qname}/pending_tasks/${taskId}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -558,7 +605,7 @@ export async function batchDeletePendingTasks(
|
|||||||
): Promise<BatchDeleteTasksResponse> {
|
): Promise<BatchDeleteTasksResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/pending_tasks:batch_delete`,
|
url: `${getBaseUrl()}/queues/${qname}/pending_tasks:batch_delete`,
|
||||||
data: {
|
data: {
|
||||||
task_ids: taskIds,
|
task_ids: taskIds,
|
||||||
},
|
},
|
||||||
@ -571,7 +618,118 @@ export async function deleteAllPendingTasks(
|
|||||||
): Promise<DeleteAllTasksResponse> {
|
): Promise<DeleteAllTasksResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "delete",
|
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;
|
return resp.data;
|
||||||
}
|
}
|
||||||
@ -582,7 +740,7 @@ export async function runScheduledTask(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/scheduled_tasks/${taskId}:run`,
|
url: `${getBaseUrl()}/queues/${qname}/scheduled_tasks/${taskId}:run`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -592,7 +750,7 @@ export async function archiveScheduledTask(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/scheduled_tasks/${taskId}:archive`,
|
url: `${getBaseUrl()}/queues/${qname}/scheduled_tasks/${taskId}:archive`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -602,7 +760,7 @@ export async function deleteScheduledTask(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "delete",
|
method: "delete",
|
||||||
url: `${BASE_URL}/queues/${qname}/scheduled_tasks/${taskId}`,
|
url: `${getBaseUrl()}/queues/${qname}/scheduled_tasks/${taskId}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -612,7 +770,7 @@ export async function batchDeleteScheduledTasks(
|
|||||||
): Promise<BatchDeleteTasksResponse> {
|
): Promise<BatchDeleteTasksResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/scheduled_tasks:batch_delete`,
|
url: `${getBaseUrl()}/queues/${qname}/scheduled_tasks:batch_delete`,
|
||||||
data: {
|
data: {
|
||||||
task_ids: taskIds,
|
task_ids: taskIds,
|
||||||
},
|
},
|
||||||
@ -625,7 +783,7 @@ export async function deleteAllScheduledTasks(
|
|||||||
): Promise<DeleteAllTasksResponse> {
|
): Promise<DeleteAllTasksResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "delete",
|
method: "delete",
|
||||||
url: `${BASE_URL}/queues/${qname}/scheduled_tasks:delete_all`,
|
url: `${getBaseUrl()}/queues/${qname}/scheduled_tasks:delete_all`,
|
||||||
});
|
});
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
@ -636,7 +794,7 @@ export async function batchRunScheduledTasks(
|
|||||||
): Promise<BatchRunTasksResponse> {
|
): Promise<BatchRunTasksResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/scheduled_tasks:batch_run`,
|
url: `${getBaseUrl()}/queues/${qname}/scheduled_tasks:batch_run`,
|
||||||
data: {
|
data: {
|
||||||
task_ids: taskIds,
|
task_ids: taskIds,
|
||||||
},
|
},
|
||||||
@ -647,7 +805,7 @@ export async function batchRunScheduledTasks(
|
|||||||
export async function runAllScheduledTasks(qname: string): Promise<void> {
|
export async function runAllScheduledTasks(qname: string): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/scheduled_tasks:run_all`,
|
url: `${getBaseUrl()}/queues/${qname}/scheduled_tasks:run_all`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -657,7 +815,7 @@ export async function batchArchiveScheduledTasks(
|
|||||||
): Promise<BatchArchiveTasksResponse> {
|
): Promise<BatchArchiveTasksResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/scheduled_tasks:batch_archive`,
|
url: `${getBaseUrl()}/queues/${qname}/scheduled_tasks:batch_archive`,
|
||||||
data: {
|
data: {
|
||||||
task_ids: taskIds,
|
task_ids: taskIds,
|
||||||
},
|
},
|
||||||
@ -668,7 +826,7 @@ export async function batchArchiveScheduledTasks(
|
|||||||
export async function archiveAllScheduledTasks(qname: string): Promise<void> {
|
export async function archiveAllScheduledTasks(qname: string): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/scheduled_tasks:archive_all`,
|
url: `${getBaseUrl()}/queues/${qname}/scheduled_tasks:archive_all`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -678,7 +836,7 @@ export async function runRetryTask(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/retry_tasks/${taskId}:run`,
|
url: `${getBaseUrl()}/queues/${qname}/retry_tasks/${taskId}:run`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -688,7 +846,7 @@ export async function archiveRetryTask(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/retry_tasks/${taskId}:archive`,
|
url: `${getBaseUrl()}/queues/${qname}/retry_tasks/${taskId}:archive`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -698,7 +856,7 @@ export async function deleteRetryTask(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "delete",
|
method: "delete",
|
||||||
url: `${BASE_URL}/queues/${qname}/retry_tasks/${taskId}`,
|
url: `${getBaseUrl()}/queues/${qname}/retry_tasks/${taskId}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -708,7 +866,7 @@ export async function batchDeleteRetryTasks(
|
|||||||
): Promise<BatchDeleteTasksResponse> {
|
): Promise<BatchDeleteTasksResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/retry_tasks:batch_delete`,
|
url: `${getBaseUrl()}/queues/${qname}/retry_tasks:batch_delete`,
|
||||||
data: {
|
data: {
|
||||||
task_ids: taskIds,
|
task_ids: taskIds,
|
||||||
},
|
},
|
||||||
@ -721,7 +879,7 @@ export async function deleteAllRetryTasks(
|
|||||||
): Promise<DeleteAllTasksResponse> {
|
): Promise<DeleteAllTasksResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "delete",
|
method: "delete",
|
||||||
url: `${BASE_URL}/queues/${qname}/retry_tasks:delete_all`,
|
url: `${getBaseUrl()}/queues/${qname}/retry_tasks:delete_all`,
|
||||||
});
|
});
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
@ -732,7 +890,7 @@ export async function batchRunRetryTasks(
|
|||||||
): Promise<BatchRunTasksResponse> {
|
): Promise<BatchRunTasksResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/retry_tasks:batch_run`,
|
url: `${getBaseUrl()}/queues/${qname}/retry_tasks:batch_run`,
|
||||||
data: {
|
data: {
|
||||||
task_ids: taskIds,
|
task_ids: taskIds,
|
||||||
},
|
},
|
||||||
@ -743,7 +901,7 @@ export async function batchRunRetryTasks(
|
|||||||
export async function runAllRetryTasks(qname: string): Promise<void> {
|
export async function runAllRetryTasks(qname: string): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/retry_tasks:run_all`,
|
url: `${getBaseUrl()}/queues/${qname}/retry_tasks:run_all`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -753,7 +911,7 @@ export async function batchArchiveRetryTasks(
|
|||||||
): Promise<BatchArchiveTasksResponse> {
|
): Promise<BatchArchiveTasksResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/retry_tasks:batch_archive`,
|
url: `${getBaseUrl()}/queues/${qname}/retry_tasks:batch_archive`,
|
||||||
data: {
|
data: {
|
||||||
task_ids: taskIds,
|
task_ids: taskIds,
|
||||||
},
|
},
|
||||||
@ -764,7 +922,7 @@ export async function batchArchiveRetryTasks(
|
|||||||
export async function archiveAllRetryTasks(qname: string): Promise<void> {
|
export async function archiveAllRetryTasks(qname: string): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/retry_tasks:archive_all`,
|
url: `${getBaseUrl()}/queues/${qname}/retry_tasks:archive_all`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -774,7 +932,7 @@ export async function runArchivedTask(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/archived_tasks/${taskId}:run`,
|
url: `${getBaseUrl()}/queues/${qname}/archived_tasks/${taskId}:run`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -784,7 +942,7 @@ export async function deleteArchivedTask(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "delete",
|
method: "delete",
|
||||||
url: `${BASE_URL}/queues/${qname}/archived_tasks/${taskId}`,
|
url: `${getBaseUrl()}/queues/${qname}/archived_tasks/${taskId}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -794,7 +952,7 @@ export async function batchDeleteArchivedTasks(
|
|||||||
): Promise<BatchDeleteTasksResponse> {
|
): Promise<BatchDeleteTasksResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/archived_tasks:batch_delete`,
|
url: `${getBaseUrl()}/queues/${qname}/archived_tasks:batch_delete`,
|
||||||
data: {
|
data: {
|
||||||
task_ids: taskIds,
|
task_ids: taskIds,
|
||||||
},
|
},
|
||||||
@ -807,7 +965,7 @@ export async function deleteAllArchivedTasks(
|
|||||||
): Promise<DeleteAllTasksResponse> {
|
): Promise<DeleteAllTasksResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "delete",
|
method: "delete",
|
||||||
url: `${BASE_URL}/queues/${qname}/archived_tasks:delete_all`,
|
url: `${getBaseUrl()}/queues/${qname}/archived_tasks:delete_all`,
|
||||||
});
|
});
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
@ -818,7 +976,7 @@ export async function batchRunArchivedTasks(
|
|||||||
): Promise<BatchRunTasksResponse> {
|
): Promise<BatchRunTasksResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/archived_tasks:batch_run`,
|
url: `${getBaseUrl()}/queues/${qname}/archived_tasks:batch_run`,
|
||||||
data: {
|
data: {
|
||||||
task_ids: taskIds,
|
task_ids: taskIds,
|
||||||
},
|
},
|
||||||
@ -829,14 +987,48 @@ export async function batchRunArchivedTasks(
|
|||||||
export async function runAllArchivedTasks(qname: string): Promise<void> {
|
export async function runAllArchivedTasks(qname: string): Promise<void> {
|
||||||
await axios({
|
await axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
url: `${BASE_URL}/queues/${qname}/archived_tasks:run_all`,
|
url: `${getBaseUrl()}/queues/${qname}/archived_tasks:run_all`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteCompletedTask(
|
||||||
|
qname: string,
|
||||||
|
taskId: string
|
||||||
|
): Promise<void> {
|
||||||
|
await axios({
|
||||||
|
method: "delete",
|
||||||
|
url: `${getBaseUrl()}/queues/${qname}/completed_tasks/${taskId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function batchDeleteCompletedTasks(
|
||||||
|
qname: string,
|
||||||
|
taskIds: string[]
|
||||||
|
): Promise<BatchDeleteTasksResponse> {
|
||||||
|
const resp = await axios({
|
||||||
|
method: "post",
|
||||||
|
url: `${getBaseUrl()}/queues/${qname}/completed_tasks:batch_delete`,
|
||||||
|
data: {
|
||||||
|
task_ids: taskIds,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAllCompletedTasks(
|
||||||
|
qname: string
|
||||||
|
): Promise<DeleteAllTasksResponse> {
|
||||||
|
const resp = await axios({
|
||||||
|
method: "delete",
|
||||||
|
url: `${getBaseUrl()}/queues/${qname}/completed_tasks:delete_all`,
|
||||||
|
});
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
|
||||||
export async function listServers(): Promise<ListServersResponse> {
|
export async function listServers(): Promise<ListServersResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "get",
|
method: "get",
|
||||||
url: `${BASE_URL}/servers`,
|
url: `${getBaseUrl()}/servers`,
|
||||||
});
|
});
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
@ -844,7 +1036,7 @@ export async function listServers(): Promise<ListServersResponse> {
|
|||||||
export async function listSchedulerEntries(): Promise<ListSchedulerEntriesResponse> {
|
export async function listSchedulerEntries(): Promise<ListSchedulerEntriesResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "get",
|
method: "get",
|
||||||
url: `${BASE_URL}/scheduler_entries`,
|
url: `${getBaseUrl()}/scheduler_entries`,
|
||||||
});
|
});
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
@ -854,7 +1046,7 @@ export async function listSchedulerEnqueueEvents(
|
|||||||
): Promise<ListSchedulerEnqueueEventsResponse> {
|
): Promise<ListSchedulerEnqueueEventsResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "get",
|
method: "get",
|
||||||
url: `${BASE_URL}/scheduler_entries/${entryId}/enqueue_events`,
|
url: `${getBaseUrl()}/scheduler_entries/${entryId}/enqueue_events`,
|
||||||
});
|
});
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
@ -862,7 +1054,32 @@ export async function listSchedulerEnqueueEvents(
|
|||||||
export async function getRedisInfo(): Promise<RedisInfoResponse> {
|
export async function getRedisInfo(): Promise<RedisInfoResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "get",
|
method: "get",
|
||||||
url: `${BASE_URL}/redis_info`,
|
url: `${getBaseUrl()}/redis_info`,
|
||||||
|
});
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MetricsEndpointParams {
|
||||||
|
endtime: number;
|
||||||
|
duration: number;
|
||||||
|
queues?: string; // comma-separated list of queues
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMetrics(
|
||||||
|
endTime: number,
|
||||||
|
duration: number,
|
||||||
|
queues: string[]
|
||||||
|
): Promise<MetricsResponse> {
|
||||||
|
let params: MetricsEndpointParams = {
|
||||||
|
endtime: endTime,
|
||||||
|
duration: duration,
|
||||||
|
};
|
||||||
|
if (queues && queues.length > 0) {
|
||||||
|
params.queues = queues.join(",");
|
||||||
|
}
|
||||||
|
const resp = await axios({
|
||||||
|
method: "get",
|
||||||
|
url: `${getBaseUrl()}/metrics?${queryString.stringify(params)}`,
|
||||||
});
|
});
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
|
@ -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 Checkbox from "@material-ui/core/Checkbox";
|
||||||
import IconButton from "@material-ui/core/IconButton";
|
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 CancelIcon from "@material-ui/icons/Cancel";
|
||||||
|
import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined";
|
||||||
import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
|
import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
|
||||||
import SyntaxHighlighter from "./SyntaxHighlighter";
|
import React from "react";
|
||||||
import {
|
import { connect, ConnectedProps } from "react-redux";
|
||||||
listActiveTasksAsync,
|
import { useHistory } from "react-router-dom";
|
||||||
cancelActiveTaskAsync,
|
|
||||||
batchCancelActiveTasksAsync,
|
|
||||||
cancelAllActiveTasksAsync,
|
|
||||||
} from "../actions/tasksActions";
|
|
||||||
import { taskRowsPerPageChange } from "../actions/settingsActions";
|
import { taskRowsPerPageChange } from "../actions/settingsActions";
|
||||||
import { AppState } from "../store";
|
import {
|
||||||
import TablePaginationActions, {
|
batchCancelActiveTasksAsync,
|
||||||
rowsPerPageOptions,
|
cancelActiveTaskAsync,
|
||||||
} from "./TablePaginationActions";
|
cancelAllActiveTasksAsync,
|
||||||
import TableActions from "./TableActions";
|
listActiveTasksAsync,
|
||||||
import { usePolling } from "../hooks";
|
} from "../actions/tasksActions";
|
||||||
import { ActiveTaskExtended } from "../reducers/tasksReducer";
|
|
||||||
import { durationBefore, timeAgo, uuidPrefix } from "../utils";
|
|
||||||
import { TableColumn } from "../types/table";
|
|
||||||
import { taskDetailsPath } from "../paths";
|
import { taskDetailsPath } from "../paths";
|
||||||
|
import { AppState } from "../store";
|
||||||
const useStyles = makeStyles((theme) => ({
|
import { TableColumn } from "../types/table";
|
||||||
table: {
|
import { durationBefore, prettifyPayload, timeAgo, uuidPrefix } from "../utils";
|
||||||
minWidth: 650,
|
import SyntaxHighlighter from "./SyntaxHighlighter";
|
||||||
},
|
import TasksTable, { RowProps, useRowStyles } from "./TasksTable";
|
||||||
stickyHeaderCell: {
|
|
||||||
background: theme.palette.background.paper,
|
|
||||||
},
|
|
||||||
alert: {
|
|
||||||
borderTopLeftRadius: 0,
|
|
||||||
borderTopRightRadius: 0,
|
|
||||||
},
|
|
||||||
pagination: {
|
|
||||||
border: "none",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
function mapStateToProps(state: AppState) {
|
function mapStateToProps(state: AppState) {
|
||||||
return {
|
return {
|
||||||
@ -66,10 +36,10 @@ function mapStateToProps(state: AppState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
listActiveTasksAsync,
|
listTasks: listActiveTasksAsync,
|
||||||
cancelActiveTaskAsync,
|
cancelTask: cancelActiveTaskAsync,
|
||||||
batchCancelActiveTasksAsync,
|
batchCancelTasks: batchCancelActiveTasksAsync,
|
||||||
cancelAllActiveTasksAsync,
|
cancelAllTasks: cancelAllActiveTasksAsync,
|
||||||
taskRowsPerPageChange,
|
taskRowsPerPageChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -89,198 +59,7 @@ type ReduxProps = ConnectedProps<typeof connector>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
queue: string; // name of the queue
|
queue: string; // name of the queue
|
||||||
}
|
totalTaskCount: number; // total number of active tasks
|
||||||
|
|
||||||
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 handleChangePage = (
|
|
||||||
event: React.MouseEvent<HTMLButtonElement> | null,
|
|
||||||
newPage: number
|
|
||||||
) => {
|
|
||||||
setPage(newPage);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeRowsPerPage = (
|
|
||||||
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,
|
|
||||||
}}
|
|
||||||
onChangePage={handleChangePage}
|
|
||||||
onChangeRowsPerPage={handleChangeRowsPerPage}
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Row(props: RowProps) {
|
function Row(props: RowProps) {
|
||||||
@ -294,18 +73,34 @@ function Row(props: RowProps) {
|
|||||||
selected={props.isSelected}
|
selected={props.isSelected}
|
||||||
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
||||||
>
|
>
|
||||||
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
{!window.READ_ONLY && (
|
||||||
<IconButton>
|
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
||||||
<Checkbox
|
<IconButton>
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
<Checkbox
|
||||||
props.onSelectChange(event.target.checked)
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
}
|
props.onSelectChange(event.target.checked)
|
||||||
checked={props.isSelected}
|
}
|
||||||
/>
|
checked={props.isSelected}
|
||||||
</IconButton>
|
/>
|
||||||
</TableCell>
|
</IconButton>
|
||||||
<TableCell component="th" scope="row">
|
</TableCell>
|
||||||
{uuidPrefix(task.id)}
|
)}
|
||||||
|
<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>
|
||||||
<TableCell>{task.type}</TableCell>
|
<TableCell>{task.type}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@ -313,42 +108,67 @@ function Row(props: RowProps) {
|
|||||||
language="json"
|
language="json"
|
||||||
customStyle={{ margin: 0, maxWidth: 400 }}
|
customStyle={{ margin: 0, maxWidth: 400 }}
|
||||||
>
|
>
|
||||||
{task.payload}
|
{prettifyPayload(task.payload)}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{task.canceling ? "Canceling" : "Running"}</TableCell>
|
|
||||||
<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>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{task.deadline === "-" ? "-" : durationBefore(task.deadline)}
|
{task.deadline === "-" ? "-" : durationBefore(task.deadline)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
{!window.READ_ONLY && (
|
||||||
align="center"
|
<TableCell
|
||||||
onMouseEnter={props.onActionCellEnter}
|
align="center"
|
||||||
onMouseLeave={props.onActionCellLeave}
|
onMouseEnter={props.onActionCellEnter}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onMouseLeave={props.onActionCellLeave}
|
||||||
>
|
onClick={(e) => e.stopPropagation()}
|
||||||
{props.showActions ? (
|
>
|
||||||
<React.Fragment>
|
{props.showActions ? (
|
||||||
<Tooltip title="Cancel">
|
<React.Fragment>
|
||||||
<IconButton
|
<Tooltip title="Cancel">
|
||||||
onClick={props.onCancelClick}
|
<IconButton
|
||||||
disabled={task.requestPending || task.canceling}
|
onClick={props.onCancelClick}
|
||||||
size="small"
|
disabled={
|
||||||
>
|
task.requestPending || task.canceling || task.is_orphaned
|
||||||
<CancelIcon fontSize="small" />
|
}
|
||||||
</IconButton>
|
size="small"
|
||||||
</Tooltip>
|
>
|
||||||
</React.Fragment>
|
<CancelIcon fontSize="small" />
|
||||||
) : (
|
</IconButton>
|
||||||
<IconButton size="small" onClick={props.onActionCellEnter}>
|
</Tooltip>
|
||||||
<MoreHorizIcon fontSize="small" />
|
</React.Fragment>
|
||||||
</IconButton>
|
) : (
|
||||||
)}
|
<IconButton size="small" onClick={props.onActionCellEnter}>
|
||||||
</TableCell>
|
<MoreHorizIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ActiveTasksTable(props: Props & ReduxProps) {
|
||||||
|
return (
|
||||||
|
<TasksTable
|
||||||
|
taskState="active"
|
||||||
|
columns={columns}
|
||||||
|
renderRow={(rowProps: RowProps) => <Row {...rowProps} />}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default connector(ActiveTasksTable);
|
export default connector(ActiveTasksTable);
|
||||||
|
247
ui/src/components/AggregatingTasksTable.tsx
Normal file
247
ui/src/components/AggregatingTasksTable.tsx
Normal 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);
|
100
ui/src/components/AggregatingTasksTableContainer.tsx
Normal file
100
ui/src/components/AggregatingTasksTableContainer.tsx
Normal 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);
|
@ -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 Checkbox from "@material-ui/core/Checkbox";
|
||||||
import TableContainer from "@material-ui/core/TableContainer";
|
import IconButton from "@material-ui/core/IconButton";
|
||||||
import TableHead from "@material-ui/core/TableHead";
|
import TableCell from "@material-ui/core/TableCell";
|
||||||
import TableRow from "@material-ui/core/TableRow";
|
import TableRow from "@material-ui/core/TableRow";
|
||||||
import Tooltip from "@material-ui/core/Tooltip";
|
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 DeleteIcon from "@material-ui/icons/Delete";
|
||||||
|
import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined";
|
||||||
import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
|
import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
|
||||||
import TableFooter from "@material-ui/core/TableFooter";
|
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
|
||||||
import TablePagination from "@material-ui/core/TablePagination";
|
import React from "react";
|
||||||
import Alert from "@material-ui/lab/Alert";
|
import { connect, ConnectedProps } from "react-redux";
|
||||||
import AlertTitle from "@material-ui/lab/AlertTitle";
|
import { useHistory } from "react-router-dom";
|
||||||
import SyntaxHighlighter from "./SyntaxHighlighter";
|
import { taskRowsPerPageChange } from "../actions/settingsActions";
|
||||||
import { AppState } from "../store";
|
|
||||||
import {
|
import {
|
||||||
batchDeleteArchivedTasksAsync,
|
batchDeleteArchivedTasksAsync,
|
||||||
batchRunArchivedTasksAsync,
|
batchRunArchivedTasksAsync,
|
||||||
deleteArchivedTaskAsync,
|
|
||||||
deleteAllArchivedTasksAsync,
|
deleteAllArchivedTasksAsync,
|
||||||
|
deleteArchivedTaskAsync,
|
||||||
listArchivedTasksAsync,
|
listArchivedTasksAsync,
|
||||||
runArchivedTaskAsync,
|
|
||||||
runAllArchivedTasksAsync,
|
runAllArchivedTasksAsync,
|
||||||
|
runArchivedTaskAsync,
|
||||||
} from "../actions/tasksActions";
|
} from "../actions/tasksActions";
|
||||||
import TablePaginationActions, {
|
|
||||||
rowsPerPageOptions,
|
|
||||||
} from "./TablePaginationActions";
|
|
||||||
import { taskRowsPerPageChange } from "../actions/settingsActions";
|
|
||||||
import TableActions from "./TableActions";
|
|
||||||
import { timeAgo, uuidPrefix } from "../utils";
|
|
||||||
import { usePolling } from "../hooks";
|
|
||||||
import { ArchivedTaskExtended } from "../reducers/tasksReducer";
|
|
||||||
import { TableColumn } from "../types/table";
|
|
||||||
import { taskDetailsPath } from "../paths";
|
import { taskDetailsPath } from "../paths";
|
||||||
|
import { AppState } from "../store";
|
||||||
const useStyles = makeStyles((theme) => ({
|
import { TableColumn } from "../types/table";
|
||||||
table: {
|
import { prettifyPayload, timeAgo, uuidPrefix } from "../utils";
|
||||||
minWidth: 650,
|
import SyntaxHighlighter from "./SyntaxHighlighter";
|
||||||
},
|
import TasksTable, { RowProps, useRowStyles } from "./TasksTable";
|
||||||
stickyHeaderCell: {
|
|
||||||
background: theme.palette.background.paper,
|
|
||||||
},
|
|
||||||
alert: {
|
|
||||||
borderTopLeftRadius: 0,
|
|
||||||
borderTopRightRadius: 0,
|
|
||||||
},
|
|
||||||
pagination: {
|
|
||||||
border: "none",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
function mapStateToProps(state: AppState) {
|
function mapStateToProps(state: AppState) {
|
||||||
return {
|
return {
|
||||||
@ -70,13 +40,13 @@ function mapStateToProps(state: AppState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
listArchivedTasksAsync,
|
listTasks: listArchivedTasksAsync,
|
||||||
runArchivedTaskAsync,
|
runTask: runArchivedTaskAsync,
|
||||||
runAllArchivedTasksAsync,
|
runAllTasks: runAllArchivedTasksAsync,
|
||||||
deleteArchivedTaskAsync,
|
deleteTask: deleteArchivedTaskAsync,
|
||||||
deleteAllArchivedTasksAsync,
|
deleteAllTasks: deleteAllArchivedTasksAsync,
|
||||||
batchRunArchivedTasksAsync,
|
batchRunTasks: batchRunArchivedTasksAsync,
|
||||||
batchDeleteArchivedTasksAsync,
|
batchDeleteTasks: batchDeleteArchivedTasksAsync,
|
||||||
taskRowsPerPageChange,
|
taskRowsPerPageChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -89,238 +59,14 @@ interface Props {
|
|||||||
totalTaskCount: number; // totoal number of archived tasks.
|
totalTaskCount: number; // totoal number of archived tasks.
|
||||||
}
|
}
|
||||||
|
|
||||||
function ArchivedTasksTable(props: Props & ReduxProps) {
|
const columns: TableColumn[] = [
|
||||||
const { pollInterval, listArchivedTasksAsync, queue, pageSize } = props;
|
{ key: "id", label: "ID", align: "left" },
|
||||||
const classes = useStyles();
|
{ key: "type", label: "Type", align: "left" },
|
||||||
const [page, setPage] = useState(0);
|
{ key: "payload", label: "Payload", align: "left" },
|
||||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
{ key: "last_failed", label: "Last Failed", align: "left" },
|
||||||
const [activeTaskId, setActiveTaskId] = useState<string>("");
|
{ key: "last_error", label: "Last Error", align: "left" },
|
||||||
|
{ key: "actions", label: "Actions", align: "center" },
|
||||||
const handleChangePage = (
|
];
|
||||||
event: React.MouseEvent<HTMLButtonElement> | null,
|
|
||||||
newPage: number
|
|
||||||
) => {
|
|
||||||
setPage(newPage);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeRowsPerPage = (
|
|
||||||
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,
|
|
||||||
}}
|
|
||||||
onChangePage={handleChangePage}
|
|
||||||
onChangeRowsPerPage={handleChangeRowsPerPage}
|
|
||||||
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: ArchivedTaskExtended;
|
|
||||||
isSelected: boolean;
|
|
||||||
onSelectChange: (checked: boolean) => void;
|
|
||||||
onRunClick: () => void;
|
|
||||||
onDeleteClick: () => void;
|
|
||||||
allActionPending: boolean;
|
|
||||||
showActions: boolean;
|
|
||||||
onActionCellEnter: () => void;
|
|
||||||
onActionCellLeave: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Row(props: RowProps) {
|
function Row(props: RowProps) {
|
||||||
const { task } = props;
|
const { task } = props;
|
||||||
@ -333,18 +79,34 @@ function Row(props: RowProps) {
|
|||||||
selected={props.isSelected}
|
selected={props.isSelected}
|
||||||
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
||||||
>
|
>
|
||||||
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
{!window.READ_ONLY && (
|
||||||
<IconButton>
|
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
||||||
<Checkbox
|
<IconButton>
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
<Checkbox
|
||||||
props.onSelectChange(event.target.checked)
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
}
|
props.onSelectChange(event.target.checked)
|
||||||
checked={props.isSelected}
|
}
|
||||||
/>
|
checked={props.isSelected}
|
||||||
</IconButton>
|
/>
|
||||||
</TableCell>
|
</IconButton>
|
||||||
<TableCell component="th" scope="row">
|
</TableCell>
|
||||||
{uuidPrefix(task.id)}
|
)}
|
||||||
|
<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>
|
||||||
<TableCell>{task.type}</TableCell>
|
<TableCell>{task.type}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@ -352,49 +114,62 @@ function Row(props: RowProps) {
|
|||||||
language="json"
|
language="json"
|
||||||
customStyle={{ margin: 0, maxWidth: 400 }}
|
customStyle={{ margin: 0, maxWidth: 400 }}
|
||||||
>
|
>
|
||||||
{task.payload}
|
{prettifyPayload(task.payload)}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{timeAgo(task.last_failed_at)}</TableCell>
|
<TableCell>{timeAgo(task.last_failed_at)}</TableCell>
|
||||||
<TableCell>{task.error_message}</TableCell>
|
<TableCell>{task.error_message}</TableCell>
|
||||||
<TableCell
|
{!window.READ_ONLY && (
|
||||||
align="center"
|
<TableCell
|
||||||
className={classes.actionCell}
|
align="center"
|
||||||
onMouseEnter={props.onActionCellEnter}
|
className={classes.actionCell}
|
||||||
onMouseLeave={props.onActionCellLeave}
|
onMouseEnter={props.onActionCellEnter}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onMouseLeave={props.onActionCellLeave}
|
||||||
>
|
onClick={(e) => e.stopPropagation()}
|
||||||
{props.showActions ? (
|
>
|
||||||
<React.Fragment>
|
{props.showActions ? (
|
||||||
<Tooltip title="Delete">
|
<React.Fragment>
|
||||||
<IconButton
|
<Tooltip title="Delete">
|
||||||
className={classes.actionButton}
|
<IconButton
|
||||||
onClick={props.onDeleteClick}
|
className={classes.actionButton}
|
||||||
disabled={task.requestPending || props.allActionPending}
|
onClick={props.onDeleteClick}
|
||||||
size="small"
|
disabled={task.requestPending || props.allActionPending}
|
||||||
>
|
size="small"
|
||||||
<DeleteIcon fontSize="small" />
|
>
|
||||||
</IconButton>
|
<DeleteIcon fontSize="small" />
|
||||||
</Tooltip>
|
</IconButton>
|
||||||
<Tooltip title="Run">
|
</Tooltip>
|
||||||
<IconButton
|
<Tooltip title="Run">
|
||||||
className={classes.actionButton}
|
<IconButton
|
||||||
onClick={props.onRunClick}
|
className={classes.actionButton}
|
||||||
disabled={task.requestPending || props.allActionPending}
|
onClick={props.onRunClick}
|
||||||
size="small"
|
disabled={task.requestPending || props.allActionPending}
|
||||||
>
|
size="small"
|
||||||
<PlayArrowIcon fontSize="small" />
|
>
|
||||||
</IconButton>
|
<PlayArrowIcon fontSize="small" />
|
||||||
</Tooltip>
|
</IconButton>
|
||||||
</React.Fragment>
|
</Tooltip>
|
||||||
) : (
|
</React.Fragment>
|
||||||
<IconButton size="small" onClick={props.onActionCellEnter}>
|
) : (
|
||||||
<MoreHorizIcon fontSize="small" />
|
<IconButton size="small" onClick={props.onActionCellEnter}>
|
||||||
</IconButton>
|
<MoreHorizIcon fontSize="small" />
|
||||||
)}
|
</IconButton>
|
||||||
</TableCell>
|
)}
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ArchivedTasksTable(props: Props & ReduxProps) {
|
||||||
|
return (
|
||||||
|
<TasksTable
|
||||||
|
taskState="archived"
|
||||||
|
columns={columns}
|
||||||
|
renderRow={(rowProps: RowProps) => <Row {...rowProps} />}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default connector(ArchivedTasksTable);
|
export default connector(ArchivedTasksTable);
|
||||||
|
176
ui/src/components/CompletedTasksTable.tsx
Normal file
176
ui/src/components/CompletedTasksTable.tsx
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
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 DeleteIcon from "@material-ui/icons/Delete";
|
||||||
|
import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined";
|
||||||
|
import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
|
||||||
|
import React from "react";
|
||||||
|
import { connect, ConnectedProps } from "react-redux";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { taskRowsPerPageChange } from "../actions/settingsActions";
|
||||||
|
import {
|
||||||
|
batchDeleteCompletedTasksAsync,
|
||||||
|
deleteAllCompletedTasksAsync,
|
||||||
|
deleteCompletedTaskAsync,
|
||||||
|
listCompletedTasksAsync,
|
||||||
|
} from "../actions/tasksActions";
|
||||||
|
import { taskDetailsPath } from "../paths";
|
||||||
|
import { AppState } from "../store";
|
||||||
|
import { TableColumn } from "../types/table";
|
||||||
|
import {
|
||||||
|
durationFromSeconds,
|
||||||
|
prettifyPayload,
|
||||||
|
stringifyDuration,
|
||||||
|
timeAgo,
|
||||||
|
uuidPrefix,
|
||||||
|
} from "../utils";
|
||||||
|
import SyntaxHighlighter from "./SyntaxHighlighter";
|
||||||
|
import TasksTable, { RowProps, useRowStyles } from "./TasksTable";
|
||||||
|
|
||||||
|
function mapStateToProps(state: AppState) {
|
||||||
|
return {
|
||||||
|
loading: state.tasks.completedTasks.loading,
|
||||||
|
error: state.tasks.completedTasks.error,
|
||||||
|
tasks: state.tasks.completedTasks.data,
|
||||||
|
batchActionPending: state.tasks.completedTasks.batchActionPending,
|
||||||
|
allActionPending: state.tasks.completedTasks.allActionPending,
|
||||||
|
pollInterval: state.settings.pollInterval,
|
||||||
|
pageSize: state.settings.taskRowsPerPage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
listTasks: listCompletedTasksAsync,
|
||||||
|
deleteTask: deleteCompletedTaskAsync,
|
||||||
|
deleteAllTasks: deleteAllCompletedTasksAsync,
|
||||||
|
batchDeleteTasks: batchDeleteCompletedTasksAsync,
|
||||||
|
taskRowsPerPageChange,
|
||||||
|
};
|
||||||
|
|
||||||
|
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
|
type ReduxProps = ConnectedProps<typeof connector>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
queue: string; // name of the queue.
|
||||||
|
totalTaskCount: number; // totoal number of completed tasks.
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
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>{timeAgo(task.completed_at)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
language="json"
|
||||||
|
customStyle={{ margin: 0, maxWidth: 400 }}
|
||||||
|
>
|
||||||
|
{prettifyPayload(task.result)}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{task.ttl_seconds > 0
|
||||||
|
? `${stringifyDuration(durationFromSeconds(task.ttl_seconds))} left`
|
||||||
|
: `expired`}
|
||||||
|
</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);
|
@ -30,8 +30,12 @@ export default function DailyStatsChart(props: Props) {
|
|||||||
<ResponsiveContainer>
|
<ResponsiveContainer>
|
||||||
<LineChart data={data}>
|
<LineChart data={data}>
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
<XAxis dataKey="date" minTickGap={10} />
|
<XAxis
|
||||||
<YAxis />
|
dataKey="date"
|
||||||
|
minTickGap={10}
|
||||||
|
stroke={theme.palette.text.secondary}
|
||||||
|
/>
|
||||||
|
<YAxis stroke={theme.palette.text.secondary} />
|
||||||
<Tooltip />
|
<Tooltip />
|
||||||
<Legend />
|
<Legend />
|
||||||
<Line
|
<Line
|
||||||
|
160
ui/src/components/GroupSelect.tsx
Normal file
160
ui/src/components/GroupSelect.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
736
ui/src/components/MetricsFetchControls.tsx
Normal file
736
ui/src/components/MetricsFetchControls.tsx
Normal file
@ -0,0 +1,736 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { connect, ConnectedProps } from "react-redux";
|
||||||
|
import { makeStyles, Theme } from "@material-ui/core/styles";
|
||||||
|
import Button, { ButtonProps } from "@material-ui/core/Button";
|
||||||
|
import ButtonGroup from "@material-ui/core/ButtonGroup";
|
||||||
|
import IconButton from "@material-ui/core/IconButton";
|
||||||
|
import Popover from "@material-ui/core/Popover";
|
||||||
|
import Radio from "@material-ui/core/Radio";
|
||||||
|
import RadioGroup from "@material-ui/core/RadioGroup";
|
||||||
|
import Checkbox from "@material-ui/core/Checkbox";
|
||||||
|
import FormControlLabel from "@material-ui/core/FormControlLabel";
|
||||||
|
import FormControl from "@material-ui/core/FormControl";
|
||||||
|
import FormGroup from "@material-ui/core/FormGroup";
|
||||||
|
import FormLabel from "@material-ui/core/FormLabel";
|
||||||
|
import TextField from "@material-ui/core/TextField";
|
||||||
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
import ArrowLeftIcon from "@material-ui/icons/ArrowLeft";
|
||||||
|
import ArrowRightIcon from "@material-ui/icons/ArrowRight";
|
||||||
|
import FilterListIcon from "@material-ui/icons/FilterList";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { currentUnixtime, parseDuration } from "../utils";
|
||||||
|
import { AppState } from "../store";
|
||||||
|
import { isDarkTheme } from "../theme";
|
||||||
|
|
||||||
|
function mapStateToProps(state: AppState) {
|
||||||
|
return { pollInterval: state.settings.pollInterval };
|
||||||
|
}
|
||||||
|
|
||||||
|
const connector = connect(mapStateToProps);
|
||||||
|
type ReduxProps = ConnectedProps<typeof connector>;
|
||||||
|
|
||||||
|
interface Props extends ReduxProps {
|
||||||
|
// Specifies the endtime in Unix time seconds.
|
||||||
|
endTimeSec: number;
|
||||||
|
onEndTimeChange: (t: number, isEndTimeFixed: boolean) => void;
|
||||||
|
|
||||||
|
// Specifies the duration in seconds.
|
||||||
|
durationSec: number;
|
||||||
|
onDurationChange: (d: number, isEndTimeFixed: boolean) => void;
|
||||||
|
|
||||||
|
// All available queues.
|
||||||
|
queues: string[];
|
||||||
|
// Selected queues.
|
||||||
|
selectedQueues: string[];
|
||||||
|
addQueue: (qname: string) => void;
|
||||||
|
removeQueue: (qname: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
endTimeOption: EndTimeOption;
|
||||||
|
durationOption: DurationOption;
|
||||||
|
customEndTime: string; // text shown in input field
|
||||||
|
customDuration: string; // text shown in input field
|
||||||
|
customEndTimeError: string;
|
||||||
|
customDurationError: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type EndTimeOption = "real_time" | "freeze_at_now" | "custom";
|
||||||
|
type DurationOption = "1h" | "6h" | "1d" | "8d" | "30d" | "custom";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
endTimeCaption: {
|
||||||
|
marginRight: theme.spacing(1),
|
||||||
|
},
|
||||||
|
shiftButtons: {
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
},
|
||||||
|
buttonGroupRoot: {
|
||||||
|
height: 29,
|
||||||
|
position: "relative",
|
||||||
|
top: 1,
|
||||||
|
},
|
||||||
|
endTimeShiftControls: {
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderBottomColor: theme.palette.divider,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomStyle: "solid",
|
||||||
|
},
|
||||||
|
leftShiftButtons: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
},
|
||||||
|
rightShiftButtons: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
marginLeft: theme.spacing(2),
|
||||||
|
},
|
||||||
|
controlsContainer: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
},
|
||||||
|
controlSelectorBox: {
|
||||||
|
display: "flex",
|
||||||
|
minWidth: 490,
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
},
|
||||||
|
controlEndTimeSelector: {
|
||||||
|
width: "50%",
|
||||||
|
},
|
||||||
|
controlDurationSelector: {
|
||||||
|
width: "50%",
|
||||||
|
},
|
||||||
|
radioButtonRoot: {
|
||||||
|
paddingTop: theme.spacing(0.5),
|
||||||
|
paddingBottom: theme.spacing(0.5),
|
||||||
|
paddingLeft: theme.spacing(1),
|
||||||
|
paddingRight: theme.spacing(1),
|
||||||
|
},
|
||||||
|
formControlLabel: {
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
buttonLabel: {
|
||||||
|
textTransform: "none",
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
formControlRoot: {
|
||||||
|
width: "100%",
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
formLabel: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 500,
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
},
|
||||||
|
customInputField: {
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
},
|
||||||
|
filterButton: {
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
},
|
||||||
|
queueFilters: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
maxHeight: 400,
|
||||||
|
},
|
||||||
|
checkbox: {
|
||||||
|
padding: 6,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// minute, hour, day in seconds
|
||||||
|
const minute = 60;
|
||||||
|
const hour = 60 * minute;
|
||||||
|
const day = 24 * hour;
|
||||||
|
|
||||||
|
function getInitialState(endTimeSec: number, durationSec: number): State {
|
||||||
|
let endTimeOption: EndTimeOption = "real_time";
|
||||||
|
let customEndTime = "";
|
||||||
|
let durationOption: DurationOption = "1h";
|
||||||
|
let customDuration = "";
|
||||||
|
|
||||||
|
const now = currentUnixtime();
|
||||||
|
// Account for 1s difference, may just happen to elapse 1s
|
||||||
|
// between the parent component's render and this component's render.
|
||||||
|
if (now <= endTimeSec && endTimeSec <= now + 1) {
|
||||||
|
endTimeOption = "real_time";
|
||||||
|
} else {
|
||||||
|
endTimeOption = "custom";
|
||||||
|
customEndTime = new Date(endTimeSec * 1000).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (durationSec) {
|
||||||
|
case 1 * hour:
|
||||||
|
durationOption = "1h";
|
||||||
|
break;
|
||||||
|
case 6 * hour:
|
||||||
|
durationOption = "6h";
|
||||||
|
break;
|
||||||
|
case 1 * day:
|
||||||
|
durationOption = "1d";
|
||||||
|
break;
|
||||||
|
case 8 * day:
|
||||||
|
durationOption = "8d";
|
||||||
|
break;
|
||||||
|
case 30 * day:
|
||||||
|
durationOption = "30d";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
durationOption = "custom";
|
||||||
|
customDuration = durationSec + "s";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
endTimeOption,
|
||||||
|
customEndTime,
|
||||||
|
customEndTimeError: "",
|
||||||
|
durationOption,
|
||||||
|
customDuration,
|
||||||
|
customDurationError: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function MetricsFetchControls(props: Props) {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const [state, setState] = React.useState<State>(
|
||||||
|
getInitialState(props.endTimeSec, props.durationSec)
|
||||||
|
);
|
||||||
|
const [timePopoverAnchorElem, setTimePopoverAnchorElem] =
|
||||||
|
React.useState<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
|
const [queuePopoverAnchorElem, setQueuePopoverAnchorElem] =
|
||||||
|
React.useState<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
|
const handleEndTimeOptionChange = (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
const selectedOpt = (event.target as HTMLInputElement)
|
||||||
|
.value as EndTimeOption;
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
endTimeOption: selectedOpt,
|
||||||
|
customEndTime: "",
|
||||||
|
customEndTimeError: "",
|
||||||
|
}));
|
||||||
|
switch (selectedOpt) {
|
||||||
|
case "real_time":
|
||||||
|
props.onEndTimeChange(currentUnixtime(), /*isEndTimeFixed=*/ false);
|
||||||
|
break;
|
||||||
|
case "freeze_at_now":
|
||||||
|
props.onEndTimeChange(currentUnixtime(), /*isEndTimeFixed=*/ true);
|
||||||
|
break;
|
||||||
|
case "custom":
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDurationOptionChange = (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
const selectedOpt = (event.target as HTMLInputElement)
|
||||||
|
.value as DurationOption;
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
durationOption: selectedOpt,
|
||||||
|
customDuration: "",
|
||||||
|
customDurationError: "",
|
||||||
|
}));
|
||||||
|
const isEndTimeFixed = state.endTimeOption !== "real_time";
|
||||||
|
switch (selectedOpt) {
|
||||||
|
case "1h":
|
||||||
|
props.onDurationChange(1 * hour, isEndTimeFixed);
|
||||||
|
break;
|
||||||
|
case "6h":
|
||||||
|
props.onDurationChange(6 * hour, isEndTimeFixed);
|
||||||
|
break;
|
||||||
|
case "1d":
|
||||||
|
props.onDurationChange(1 * day, isEndTimeFixed);
|
||||||
|
break;
|
||||||
|
case "8d":
|
||||||
|
props.onDurationChange(8 * day, isEndTimeFixed);
|
||||||
|
break;
|
||||||
|
case "30d":
|
||||||
|
props.onDurationChange(30 * day, isEndTimeFixed);
|
||||||
|
break;
|
||||||
|
case "custom":
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCustomDurationChange = (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
event.persist(); // https://reactjs.org/docs/legacy-event-pooling.html
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
customDuration: event.target.value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCustomEndTimeChange = (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
event.persist(); // https://reactjs.org/docs/legacy-event-pooling.html
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
customEndTime: event.target.value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCustomDurationKeyDown = (
|
||||||
|
event: React.KeyboardEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
try {
|
||||||
|
const d = parseDuration(state.customDuration);
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
durationOption: "custom",
|
||||||
|
customDurationError: "",
|
||||||
|
}));
|
||||||
|
props.onDurationChange(d, state.endTimeOption !== "real_time");
|
||||||
|
} catch (error) {
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
customDurationError: "Duration invalid",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCustomEndTimeKeyDown = (
|
||||||
|
event: React.KeyboardEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
const timeUsecOrNaN = Date.parse(state.customEndTime);
|
||||||
|
if (isNaN(timeUsecOrNaN)) {
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
customEndTimeError: "End time invalid",
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
endTimeOption: "custom",
|
||||||
|
customEndTimeError: "",
|
||||||
|
}));
|
||||||
|
props.onEndTimeChange(
|
||||||
|
Math.floor(timeUsecOrNaN / 1000),
|
||||||
|
/* isEndTimeFixed= */ true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenTimePopover = (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement>
|
||||||
|
) => {
|
||||||
|
setTimePopoverAnchorElem(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseTimePopover = () => {
|
||||||
|
setTimePopoverAnchorElem(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenQueuePopover = (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement>
|
||||||
|
) => {
|
||||||
|
setQueuePopoverAnchorElem(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseQueuePopover = () => {
|
||||||
|
setQueuePopoverAnchorElem(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTimePopoverOpen = Boolean(timePopoverAnchorElem);
|
||||||
|
const isQueuePopoverOpen = Boolean(queuePopoverAnchorElem);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (state.endTimeOption === "real_time") {
|
||||||
|
const id = setInterval(() => {
|
||||||
|
props.onEndTimeChange(currentUnixtime(), /*isEndTimeFixed=*/ false);
|
||||||
|
}, props.pollInterval * 1000);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const shiftBy = (deltaSec: number) => {
|
||||||
|
return () => {
|
||||||
|
const now = currentUnixtime();
|
||||||
|
const endTime = props.endTimeSec + deltaSec;
|
||||||
|
if (now <= endTime) {
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
customEndTime: "",
|
||||||
|
endTimeOption: "real_time",
|
||||||
|
}));
|
||||||
|
props.onEndTimeChange(now, /*isEndTimeFixed=*/ false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
endTimeOption: "custom",
|
||||||
|
customEndTime: new Date(endTime * 1000).toISOString(),
|
||||||
|
}));
|
||||||
|
props.onEndTimeChange(endTime, /*isEndTimeFixed=*/ true);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="textPrimary"
|
||||||
|
className={classes.endTimeCaption}
|
||||||
|
>
|
||||||
|
{formatTime(props.endTimeSec)}
|
||||||
|
</Typography>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
aria-describedby={isTimePopoverOpen ? "time-popover" : undefined}
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
onClick={handleOpenTimePopover}
|
||||||
|
size="small"
|
||||||
|
classes={{
|
||||||
|
label: classes.buttonLabel,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{state.endTimeOption === "real_time" ? "Realtime" : "Historical"}:{" "}
|
||||||
|
{state.durationOption === "custom"
|
||||||
|
? state.customDuration
|
||||||
|
: state.durationOption}
|
||||||
|
</Button>
|
||||||
|
<Popover
|
||||||
|
id={isTimePopoverOpen ? "time-popover" : undefined}
|
||||||
|
open={isTimePopoverOpen}
|
||||||
|
anchorEl={timePopoverAnchorElem}
|
||||||
|
onClose={handleCloseTimePopover}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "bottom",
|
||||||
|
horizontal: "center",
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: "top",
|
||||||
|
horizontal: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={classes.endTimeShiftControls}>
|
||||||
|
<div className={classes.leftShiftButtons}>
|
||||||
|
<ShiftButton
|
||||||
|
direction="left"
|
||||||
|
text="2h"
|
||||||
|
onClick={shiftBy(-2 * hour)}
|
||||||
|
dense={true}
|
||||||
|
/>
|
||||||
|
<ShiftButton
|
||||||
|
direction="left"
|
||||||
|
text="1h"
|
||||||
|
onClick={shiftBy(-1 * hour)}
|
||||||
|
dense={true}
|
||||||
|
/>
|
||||||
|
<ShiftButton
|
||||||
|
direction="left"
|
||||||
|
text="30m"
|
||||||
|
onClick={shiftBy(-30 * minute)}
|
||||||
|
dense={true}
|
||||||
|
/>
|
||||||
|
<ShiftButton
|
||||||
|
direction="left"
|
||||||
|
text="15m"
|
||||||
|
onClick={shiftBy(-15 * minute)}
|
||||||
|
dense={true}
|
||||||
|
/>
|
||||||
|
<ShiftButton
|
||||||
|
direction="left"
|
||||||
|
text="5m"
|
||||||
|
onClick={shiftBy(-5 * minute)}
|
||||||
|
dense={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={classes.rightShiftButtons}>
|
||||||
|
<ShiftButton
|
||||||
|
direction="right"
|
||||||
|
text="5m"
|
||||||
|
onClick={shiftBy(5 * minute)}
|
||||||
|
dense={true}
|
||||||
|
/>
|
||||||
|
<ShiftButton
|
||||||
|
direction="right"
|
||||||
|
text="15m"
|
||||||
|
onClick={shiftBy(15 * minute)}
|
||||||
|
dense={true}
|
||||||
|
/>
|
||||||
|
<ShiftButton
|
||||||
|
direction="right"
|
||||||
|
text="30m"
|
||||||
|
onClick={shiftBy(30 * minute)}
|
||||||
|
dense={true}
|
||||||
|
/>
|
||||||
|
<ShiftButton
|
||||||
|
direction="right"
|
||||||
|
text="1h"
|
||||||
|
onClick={shiftBy(1 * hour)}
|
||||||
|
dense={true}
|
||||||
|
/>
|
||||||
|
<ShiftButton
|
||||||
|
direction="right"
|
||||||
|
text="2h"
|
||||||
|
onClick={shiftBy(2 * hour)}
|
||||||
|
dense={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={classes.controlSelectorBox}>
|
||||||
|
<div className={classes.controlEndTimeSelector}>
|
||||||
|
<FormControl
|
||||||
|
component="fieldset"
|
||||||
|
margin="dense"
|
||||||
|
classes={{ root: classes.formControlRoot }}
|
||||||
|
>
|
||||||
|
<FormLabel className={classes.formLabel} component="legend">
|
||||||
|
End Time
|
||||||
|
</FormLabel>
|
||||||
|
<RadioGroup
|
||||||
|
aria-label="end_time"
|
||||||
|
name="end_time"
|
||||||
|
value={state.endTimeOption}
|
||||||
|
onChange={handleEndTimeOptionChange}
|
||||||
|
>
|
||||||
|
<RadioInput value="real_time" label="Real Time" />
|
||||||
|
<RadioInput value="freeze_at_now" label="Freeze at now" />
|
||||||
|
<RadioInput value="custom" label="Custom End Time" />
|
||||||
|
</RadioGroup>
|
||||||
|
<div className={classes.customInputField}>
|
||||||
|
<TextField
|
||||||
|
id="custom-endtime"
|
||||||
|
label="yyyy-mm-dd hh:mm:ssz"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onChange={handleCustomEndTimeChange}
|
||||||
|
value={state.customEndTime}
|
||||||
|
onKeyDown={handleCustomEndTimeKeyDown}
|
||||||
|
error={state.customEndTimeError !== ""}
|
||||||
|
helperText={state.customEndTimeError}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<div className={classes.controlDurationSelector}>
|
||||||
|
<FormControl
|
||||||
|
component="fieldset"
|
||||||
|
margin="dense"
|
||||||
|
classes={{ root: classes.formControlRoot }}
|
||||||
|
>
|
||||||
|
<FormLabel className={classes.formLabel} component="legend">
|
||||||
|
Duration
|
||||||
|
</FormLabel>
|
||||||
|
<RadioGroup
|
||||||
|
aria-label="duration"
|
||||||
|
name="duration"
|
||||||
|
value={state.durationOption}
|
||||||
|
onChange={handleDurationOptionChange}
|
||||||
|
>
|
||||||
|
<RadioInput value="1h" label="1h" />
|
||||||
|
<RadioInput value="6h" label="6h" />
|
||||||
|
<RadioInput value="1d" label="1 day" />
|
||||||
|
<RadioInput value="8d" label="8 days" />
|
||||||
|
<RadioInput value="30d" label="30 days" />
|
||||||
|
<RadioInput value="custom" label="Custom Duration" />
|
||||||
|
</RadioGroup>
|
||||||
|
<div className={classes.customInputField}>
|
||||||
|
<TextField
|
||||||
|
id="custom-duration"
|
||||||
|
label="duration"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onChange={handleCustomDurationChange}
|
||||||
|
value={state.customDuration}
|
||||||
|
onKeyDown={handleCustomDurationKeyDown}
|
||||||
|
error={state.customDurationError !== ""}
|
||||||
|
helperText={state.customDurationError}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<div className={classes.shiftButtons}>
|
||||||
|
<ButtonGroup
|
||||||
|
classes={{ root: classes.buttonGroupRoot }}
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
aria-label="shift buttons"
|
||||||
|
>
|
||||||
|
<ShiftButton
|
||||||
|
direction="left"
|
||||||
|
text={
|
||||||
|
state.durationOption === "custom" ? "1h" : state.durationOption
|
||||||
|
}
|
||||||
|
color="primary"
|
||||||
|
onClick={
|
||||||
|
state.durationOption === "custom"
|
||||||
|
? shiftBy(-1 * hour)
|
||||||
|
: shiftBy(-props.durationSec)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ShiftButton
|
||||||
|
direction="right"
|
||||||
|
text={
|
||||||
|
state.durationOption === "custom" ? "1h" : state.durationOption
|
||||||
|
}
|
||||||
|
color="primary"
|
||||||
|
onClick={
|
||||||
|
state.durationOption === "custom"
|
||||||
|
? shiftBy(1 * hour)
|
||||||
|
: shiftBy(props.durationSec)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
<div className={classes.filterButton}>
|
||||||
|
<IconButton
|
||||||
|
aria-label="filter"
|
||||||
|
size="small"
|
||||||
|
onClick={handleOpenQueuePopover}
|
||||||
|
>
|
||||||
|
<FilterListIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Popover
|
||||||
|
id={isQueuePopoverOpen ? "queue-popover" : undefined}
|
||||||
|
open={isQueuePopoverOpen}
|
||||||
|
anchorEl={queuePopoverAnchorElem}
|
||||||
|
onClose={handleCloseQueuePopover}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "bottom",
|
||||||
|
horizontal: "center",
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: "top",
|
||||||
|
horizontal: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl className={classes.queueFilters}>
|
||||||
|
<FormLabel className={classes.formLabel} component="legend">
|
||||||
|
Queues
|
||||||
|
</FormLabel>
|
||||||
|
<FormGroup>
|
||||||
|
{props.queues.map((qname) => (
|
||||||
|
<FormControlLabel
|
||||||
|
key={qname}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
size="small"
|
||||||
|
checked={props.selectedQueues.includes(qname)}
|
||||||
|
onChange={() => {
|
||||||
|
if (props.selectedQueues.includes(qname)) {
|
||||||
|
props.removeQueue(qname);
|
||||||
|
} else {
|
||||||
|
props.addQueue(qname);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
name={qname}
|
||||||
|
className={classes.checkbox}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={qname}
|
||||||
|
classes={{ label: classes.formControlLabel }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FormGroup>
|
||||||
|
</FormControl>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/****************** Helper functions/components *******************/
|
||||||
|
|
||||||
|
function formatTime(unixtime: number): string {
|
||||||
|
const tz = new Date(unixtime * 1000)
|
||||||
|
.toLocaleTimeString("en-us", { timeZoneName: "short" })
|
||||||
|
.split(" ")[2];
|
||||||
|
return dayjs.unix(unixtime).format("ddd, DD MMM YYYY HH:mm:ss ") + tz;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RadioInputProps {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function RadioInput(props: RadioInputProps) {
|
||||||
|
const classes = useStyles();
|
||||||
|
return (
|
||||||
|
<FormControlLabel
|
||||||
|
classes={{ label: classes.formControlLabel }}
|
||||||
|
value={props.value}
|
||||||
|
control={
|
||||||
|
<Radio size="small" classes={{ root: classes.radioButtonRoot }} />
|
||||||
|
}
|
||||||
|
label={props.label}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShiftButtonProps extends ButtonProps {
|
||||||
|
text: string;
|
||||||
|
onClick: () => void;
|
||||||
|
direction: "left" | "right";
|
||||||
|
dense?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useShiftButtonStyles = makeStyles((theme: Theme) => ({
|
||||||
|
root: {
|
||||||
|
minWidth: 40,
|
||||||
|
fontWeight: (props: ShiftButtonProps) => (props.dense ? 400 : 500),
|
||||||
|
},
|
||||||
|
label: { fontSize: 12, textTransform: "none" },
|
||||||
|
iconRoot: {
|
||||||
|
marginRight: (props: ShiftButtonProps) =>
|
||||||
|
props.direction === "left" ? (props.dense ? -8 : -4) : 0,
|
||||||
|
marginLeft: (props: ShiftButtonProps) =>
|
||||||
|
props.direction === "right" ? (props.dense ? -8 : -4) : 0,
|
||||||
|
color: (props: ShiftButtonProps) =>
|
||||||
|
props.color
|
||||||
|
? props.color
|
||||||
|
: theme.palette.grey[isDarkTheme(theme) ? 200 : 700],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
function ShiftButton(props: ShiftButtonProps) {
|
||||||
|
const classes = useShiftButtonStyles(props);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
|
classes={{
|
||||||
|
root: classes.root,
|
||||||
|
label: classes.label,
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{props.direction === "left" && (
|
||||||
|
<ArrowLeftIcon classes={{ root: classes.iconRoot }} />
|
||||||
|
)}
|
||||||
|
{props.text}
|
||||||
|
{props.direction === "right" && (
|
||||||
|
<ArrowRightIcon classes={{ root: classes.iconRoot }} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ShiftButton.defaultProps = {
|
||||||
|
dense: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(MetricsFetchControls);
|
@ -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 Checkbox from "@material-ui/core/Checkbox";
|
||||||
import IconButton from "@material-ui/core/IconButton";
|
import IconButton from "@material-ui/core/IconButton";
|
||||||
import TableFooter from "@material-ui/core/TableFooter";
|
import TableCell from "@material-ui/core/TableCell";
|
||||||
import TablePagination from "@material-ui/core/TablePagination";
|
import TableRow from "@material-ui/core/TableRow";
|
||||||
import DeleteIcon from "@material-ui/icons/Delete";
|
import Tooltip from "@material-ui/core/Tooltip";
|
||||||
import ArchiveIcon from "@material-ui/icons/Archive";
|
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 MoreHorizIcon from "@material-ui/icons/MoreHoriz";
|
||||||
import SyntaxHighlighter from "./SyntaxHighlighter";
|
import React from "react";
|
||||||
import TablePaginationActions, {
|
import { connect, ConnectedProps } from "react-redux";
|
||||||
rowsPerPageOptions,
|
import { useHistory } from "react-router-dom";
|
||||||
} from "./TablePaginationActions";
|
import { taskRowsPerPageChange } from "../actions/settingsActions";
|
||||||
import TableActions from "./TableActions";
|
|
||||||
import {
|
import {
|
||||||
listPendingTasksAsync,
|
archiveAllPendingTasksAsync,
|
||||||
deletePendingTaskAsync,
|
|
||||||
batchDeletePendingTasksAsync,
|
|
||||||
deleteAllPendingTasksAsync,
|
|
||||||
archivePendingTaskAsync,
|
archivePendingTaskAsync,
|
||||||
batchArchivePendingTasksAsync,
|
batchArchivePendingTasksAsync,
|
||||||
archiveAllPendingTasksAsync,
|
batchDeletePendingTasksAsync,
|
||||||
|
deleteAllPendingTasksAsync,
|
||||||
|
deletePendingTaskAsync,
|
||||||
|
listPendingTasksAsync,
|
||||||
} from "../actions/tasksActions";
|
} from "../actions/tasksActions";
|
||||||
import { taskRowsPerPageChange } from "../actions/settingsActions";
|
|
||||||
import { AppState } from "../store";
|
|
||||||
import { usePolling } from "../hooks";
|
|
||||||
import { uuidPrefix } from "../utils";
|
|
||||||
import { TableColumn } from "../types/table";
|
|
||||||
import { PendingTaskExtended } from "../reducers/tasksReducer";
|
|
||||||
import { taskDetailsPath } from "../paths";
|
import { taskDetailsPath } from "../paths";
|
||||||
|
import { AppState } from "../store";
|
||||||
const useStyles = makeStyles((theme) => ({
|
import { TableColumn } from "../types/table";
|
||||||
table: {
|
import { prettifyPayload, uuidPrefix } from "../utils";
|
||||||
minWidth: 650,
|
import SyntaxHighlighter from "./SyntaxHighlighter";
|
||||||
},
|
import TasksTable, { RowProps, useRowStyles } from "./TasksTable";
|
||||||
stickyHeaderCell: {
|
|
||||||
background: theme.palette.background.paper,
|
|
||||||
},
|
|
||||||
alert: {
|
|
||||||
borderTopLeftRadius: 0,
|
|
||||||
borderTopRightRadius: 0,
|
|
||||||
},
|
|
||||||
pagination: {
|
|
||||||
border: "none",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
function mapStateToProps(state: AppState) {
|
function mapStateToProps(state: AppState) {
|
||||||
return {
|
return {
|
||||||
@ -70,13 +40,13 @@ function mapStateToProps(state: AppState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
listPendingTasksAsync,
|
listTasks: listPendingTasksAsync,
|
||||||
deletePendingTaskAsync,
|
deleteTask: deletePendingTaskAsync,
|
||||||
batchDeletePendingTasksAsync,
|
batchDeleteTasks: batchDeletePendingTasksAsync,
|
||||||
deleteAllPendingTasksAsync,
|
deleteAllTasks: deleteAllPendingTasksAsync,
|
||||||
archivePendingTaskAsync,
|
archiveTask: archivePendingTaskAsync,
|
||||||
batchArchivePendingTasksAsync,
|
batchArchiveTasks: batchArchivePendingTasksAsync,
|
||||||
archiveAllPendingTasksAsync,
|
archiveAllTasks: archiveAllPendingTasksAsync,
|
||||||
taskRowsPerPageChange,
|
taskRowsPerPageChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -89,240 +59,14 @@ interface Props {
|
|||||||
totalTaskCount: number; // total number of pending tasks
|
totalTaskCount: number; // total number of pending tasks
|
||||||
}
|
}
|
||||||
|
|
||||||
function PendingTasksTable(props: Props & ReduxProps) {
|
const columns: TableColumn[] = [
|
||||||
const { pollInterval, listPendingTasksAsync, queue, pageSize } = props;
|
{ key: "id", label: "ID", align: "left" },
|
||||||
const classes = useStyles();
|
{ key: "type", label: "Type", align: "left" },
|
||||||
const [page, setPage] = useState(0);
|
{ key: "paylod", label: "Payload", align: "left" },
|
||||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
{ key: "retried", label: "Retried", align: "right" },
|
||||||
const [activeTaskId, setActiveTaskId] = useState<string>("");
|
{ key: "max_retry", label: "Max Retry", align: "right" },
|
||||||
|
{ key: "actions", label: "Actions", align: "center" },
|
||||||
const handleChangePage = (
|
];
|
||||||
event: React.MouseEvent<HTMLButtonElement> | null,
|
|
||||||
newPage: number
|
|
||||||
) => {
|
|
||||||
setPage(newPage);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeRowsPerPage = (
|
|
||||||
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,
|
|
||||||
}}
|
|
||||||
onChangePage={handleChangePage}
|
|
||||||
onChangeRowsPerPage={handleChangeRowsPerPage}
|
|
||||||
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: PendingTaskExtended;
|
|
||||||
isSelected: boolean;
|
|
||||||
onSelectChange: (checked: boolean) => void;
|
|
||||||
onDeleteClick: () => void;
|
|
||||||
onArchiveClick: () => void;
|
|
||||||
allActionPending: boolean;
|
|
||||||
showActions: boolean;
|
|
||||||
onActionCellEnter: () => void;
|
|
||||||
onActionCellLeave: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Row(props: RowProps) {
|
function Row(props: RowProps) {
|
||||||
const { task } = props;
|
const { task } = props;
|
||||||
@ -335,18 +79,34 @@ function Row(props: RowProps) {
|
|||||||
selected={props.isSelected}
|
selected={props.isSelected}
|
||||||
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
||||||
>
|
>
|
||||||
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
{!window.READ_ONLY && (
|
||||||
<IconButton>
|
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
||||||
<Checkbox
|
<IconButton>
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
<Checkbox
|
||||||
props.onSelectChange(event.target.checked)
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
}
|
props.onSelectChange(event.target.checked)
|
||||||
checked={props.isSelected}
|
}
|
||||||
/>
|
checked={props.isSelected}
|
||||||
</IconButton>
|
/>
|
||||||
</TableCell>
|
</IconButton>
|
||||||
<TableCell component="th" scope="row">
|
</TableCell>
|
||||||
{uuidPrefix(task.id)}
|
)}
|
||||||
|
<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>
|
||||||
<TableCell>{task.type}</TableCell>
|
<TableCell>{task.type}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@ -354,49 +114,62 @@ function Row(props: RowProps) {
|
|||||||
language="json"
|
language="json"
|
||||||
customStyle={{ margin: 0, maxWidth: 400 }}
|
customStyle={{ margin: 0, maxWidth: 400 }}
|
||||||
>
|
>
|
||||||
{task.payload}
|
{prettifyPayload(task.payload)}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">{task.retried}</TableCell>
|
<TableCell align="right">{task.retried}</TableCell>
|
||||||
<TableCell align="right">{task.max_retry}</TableCell>
|
<TableCell align="right">{task.max_retry}</TableCell>
|
||||||
<TableCell
|
{!window.READ_ONLY && (
|
||||||
align="center"
|
<TableCell
|
||||||
className={classes.actionCell}
|
align="center"
|
||||||
onMouseEnter={props.onActionCellEnter}
|
className={classes.actionCell}
|
||||||
onMouseLeave={props.onActionCellLeave}
|
onMouseEnter={props.onActionCellEnter}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onMouseLeave={props.onActionCellLeave}
|
||||||
>
|
onClick={(e) => e.stopPropagation()}
|
||||||
{props.showActions ? (
|
>
|
||||||
<React.Fragment>
|
{props.showActions ? (
|
||||||
<Tooltip title="Delete">
|
<React.Fragment>
|
||||||
<IconButton
|
<Tooltip title="Delete">
|
||||||
onClick={props.onDeleteClick}
|
<IconButton
|
||||||
disabled={task.requestPending || props.allActionPending}
|
onClick={props.onDeleteClick}
|
||||||
size="small"
|
disabled={task.requestPending || props.allActionPending}
|
||||||
className={classes.actionButton}
|
size="small"
|
||||||
>
|
className={classes.actionButton}
|
||||||
<DeleteIcon fontSize="small" />
|
>
|
||||||
</IconButton>
|
<DeleteIcon fontSize="small" />
|
||||||
</Tooltip>
|
</IconButton>
|
||||||
<Tooltip title="Archive">
|
</Tooltip>
|
||||||
<IconButton
|
<Tooltip title="Archive">
|
||||||
onClick={props.onArchiveClick}
|
<IconButton
|
||||||
disabled={task.requestPending || props.allActionPending}
|
onClick={props.onArchiveClick}
|
||||||
size="small"
|
disabled={task.requestPending || props.allActionPending}
|
||||||
className={classes.actionButton}
|
size="small"
|
||||||
>
|
className={classes.actionButton}
|
||||||
<ArchiveIcon fontSize="small" />
|
>
|
||||||
</IconButton>
|
<ArchiveIcon fontSize="small" />
|
||||||
</Tooltip>
|
</IconButton>
|
||||||
</React.Fragment>
|
</Tooltip>
|
||||||
) : (
|
</React.Fragment>
|
||||||
<IconButton size="small" onClick={props.onActionCellEnter}>
|
) : (
|
||||||
<MoreHorizIcon fontSize="small" />
|
<IconButton size="small" onClick={props.onActionCellEnter}>
|
||||||
</IconButton>
|
<MoreHorizIcon fontSize="small" />
|
||||||
)}
|
</IconButton>
|
||||||
</TableCell>
|
)}
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PendingTasksTable(props: Props & ReduxProps) {
|
||||||
|
return (
|
||||||
|
<TasksTable
|
||||||
|
taskState="pending"
|
||||||
|
columns={columns}
|
||||||
|
renderRow={(rowProps: RowProps) => <Row {...rowProps} />}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default connector(PendingTasksTable);
|
export default connector(PendingTasksTable);
|
||||||
|
@ -27,8 +27,8 @@ function ProcessedTasksChart(props: Props) {
|
|||||||
<ResponsiveContainer>
|
<ResponsiveContainer>
|
||||||
<BarChart data={props.data} maxBarSize={120}>
|
<BarChart data={props.data} maxBarSize={120}>
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
<XAxis dataKey="queue" />
|
<XAxis dataKey="queue" stroke={theme.palette.text.secondary} />
|
||||||
<YAxis />
|
<YAxis stroke={theme.palette.text.secondary} />
|
||||||
<Tooltip />
|
<Tooltip />
|
||||||
<Legend />
|
<Legend />
|
||||||
<Bar
|
<Bar
|
||||||
|
@ -6,7 +6,7 @@ import Chip from "@material-ui/core/Chip";
|
|||||||
import Menu from "@material-ui/core/Menu";
|
import Menu from "@material-ui/core/Menu";
|
||||||
import MenuItem from "@material-ui/core/MenuItem";
|
import MenuItem from "@material-ui/core/MenuItem";
|
||||||
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
|
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
|
||||||
import { paths, queueDetailsPath } from "../paths";
|
import { paths as getPaths, queueDetailsPath } from "../paths";
|
||||||
import { isDarkTheme } from "../theme";
|
import { isDarkTheme } from "../theme";
|
||||||
|
|
||||||
const StyledBreadcrumb = withStyles((theme: Theme) => ({
|
const StyledBreadcrumb = withStyles((theme: Theme) => ({
|
||||||
@ -16,7 +16,7 @@ const StyledBreadcrumb = withStyles((theme: Theme) => ({
|
|||||||
: theme.palette.background.default,
|
: theme.palette.background.default,
|
||||||
height: theme.spacing(3),
|
height: theme.spacing(3),
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
fontWeight: theme.typography.fontWeightRegular,
|
fontWeight: 400,
|
||||||
"&:hover, &:focus": {
|
"&:hover, &:focus": {
|
||||||
backgroundColor: theme.palette.action.hover,
|
backgroundColor: theme.palette.action.hover,
|
||||||
},
|
},
|
||||||
@ -39,6 +39,7 @@ interface Props {
|
|||||||
export default function QueueBreadcrumbs(props: Props) {
|
export default function QueueBreadcrumbs(props: Props) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [anchorEl, setAnchorEl] = useState<null | Element>(null);
|
const [anchorEl, setAnchorEl] = useState<null | Element>(null);
|
||||||
|
const paths = getPaths();
|
||||||
|
|
||||||
const handleClick = (event: React.MouseEvent<Element, MouseEvent>) => {
|
const handleClick = (event: React.MouseEvent<Element, MouseEvent>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -65,6 +65,15 @@ function QueueInfoBanner(props: Props & ReduxProps) {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</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}>
|
<div className={classes.bannerItem}>
|
||||||
<Typography variant="subtitle2" color="textPrimary" gutterBottom>
|
<Typography variant="subtitle2" color="textPrimary" gutterBottom>
|
||||||
Memory usage
|
Memory usage
|
||||||
@ -74,6 +83,15 @@ function QueueInfoBanner(props: Props & ReduxProps) {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</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}>
|
<div className={classes.bannerItem}>
|
||||||
<Typography variant="subtitle2" color="textPrimary" gutterBottom>
|
<Typography variant="subtitle2" color="textPrimary" gutterBottom>
|
||||||
Processed
|
Processed
|
||||||
|
108
ui/src/components/QueueMetricsChart.tsx
Normal file
108
ui/src/components/QueueMetricsChart.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { useTheme } from "@material-ui/core/styles";
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
LineChart,
|
||||||
|
Line,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
ResponsiveContainer,
|
||||||
|
} from "recharts";
|
||||||
|
import { Metrics } from "../api";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: Metrics[];
|
||||||
|
|
||||||
|
// both startTime and endTime are in unix time (seconds)
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
|
||||||
|
// (optional): Tick formatter function for YAxis
|
||||||
|
yAxisTickFormatter?: (val: number) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// interface that rechart understands.
|
||||||
|
interface ChartData {
|
||||||
|
timestamp: number;
|
||||||
|
[qname: string]: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toChartData(metrics: Metrics[]): ChartData[] {
|
||||||
|
if (metrics.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let byTimestamp: { [key: number]: ChartData } = {};
|
||||||
|
for (let x of metrics) {
|
||||||
|
for (let [ts, val] of x.values) {
|
||||||
|
if (!byTimestamp[ts]) {
|
||||||
|
byTimestamp[ts] = { timestamp: ts };
|
||||||
|
}
|
||||||
|
const qname = x.metric.queue;
|
||||||
|
if (qname) {
|
||||||
|
byTimestamp[ts][qname] = parseFloat(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.values(byTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineColors = [
|
||||||
|
"#2085ec",
|
||||||
|
"#72b4eb",
|
||||||
|
"#0a417a",
|
||||||
|
"#8464a0",
|
||||||
|
"#cea9bc",
|
||||||
|
"#323232",
|
||||||
|
];
|
||||||
|
|
||||||
|
function QueueMetricsChart(props: Props) {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const data = toChartData(props.data);
|
||||||
|
const keys = props.data.map((x) => x.metric.queue);
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer height={260}>
|
||||||
|
<LineChart data={data}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis
|
||||||
|
minTickGap={10}
|
||||||
|
dataKey="timestamp"
|
||||||
|
domain={[props.startTime, props.endTime]}
|
||||||
|
tickFormatter={(timestamp: number) =>
|
||||||
|
new Date(timestamp * 1000).toLocaleTimeString()
|
||||||
|
}
|
||||||
|
type="number"
|
||||||
|
scale="time"
|
||||||
|
stroke={theme.palette.text.secondary}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tickFormatter={props.yAxisTickFormatter}
|
||||||
|
stroke={theme.palette.text.secondary}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
labelFormatter={(timestamp: number) => {
|
||||||
|
return new Date(timestamp * 1000).toLocaleTimeString();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Legend />
|
||||||
|
{keys.map((key, idx) => (
|
||||||
|
<Line
|
||||||
|
key={key}
|
||||||
|
type="monotone"
|
||||||
|
dataKey={key}
|
||||||
|
stroke={lineColors[idx % lineColors.length]}
|
||||||
|
dot={false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueMetricsChart.defaultProps = {
|
||||||
|
yAxisTickFormatter: (val: number) => val.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QueueMetricsChart;
|
@ -10,6 +10,7 @@ import {
|
|||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { useTheme } from "@material-ui/core/styles";
|
||||||
import { queueDetailsPath } from "../paths";
|
import { queueDetailsPath } from "../paths";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -20,12 +21,15 @@ interface TaskBreakdown {
|
|||||||
queue: string; // name of the queue.
|
queue: string; // name of the queue.
|
||||||
active: number; // number of active tasks in the queue.
|
active: number; // number of active tasks in the queue.
|
||||||
pending: number; // number of pending 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.
|
scheduled: number; // number of scheduled tasks in the queue.
|
||||||
retry: number; // number of retry tasks in the queue.
|
retry: number; // number of retry tasks in the queue.
|
||||||
archived: number; // number of archived tasks in the queue.
|
archived: number; // number of archived tasks in the queue.
|
||||||
|
completed: number; // number of completed tasks in the queue.
|
||||||
}
|
}
|
||||||
|
|
||||||
function QueueSizeChart(props: Props) {
|
function QueueSizeChart(props: Props) {
|
||||||
|
const theme = useTheme();
|
||||||
const handleClick = (params: { activeLabel?: string } | null) => {
|
const handleClick = (params: { activeLabel?: string } | null) => {
|
||||||
const allQueues = props.data.map((b) => b.queue);
|
const allQueues = props.data.map((b) => b.queue);
|
||||||
if (
|
if (
|
||||||
@ -46,15 +50,17 @@ function QueueSizeChart(props: Props) {
|
|||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
<XAxis dataKey="queue" />
|
<XAxis dataKey="queue" stroke={theme.palette.text.secondary} />
|
||||||
<YAxis />
|
<YAxis stroke={theme.palette.text.secondary} />
|
||||||
<Tooltip />
|
<Tooltip />
|
||||||
<Legend />
|
<Legend />
|
||||||
<Bar dataKey="active" stackId="a" fill="#1967d2" />
|
<Bar dataKey="active" stackId="a" fill="#1967d2" />
|
||||||
<Bar dataKey="pending" stackId="a" fill="#669df6" />
|
<Bar dataKey="pending" stackId="a" fill="#669df6" />
|
||||||
|
<Bar dataKey="aggregating" stackId="a" fill="#e69138" />
|
||||||
<Bar dataKey="scheduled" stackId="a" fill="#fdd663" />
|
<Bar dataKey="scheduled" stackId="a" fill="#fdd663" />
|
||||||
<Bar dataKey="retry" stackId="a" fill="#f666a9" />
|
<Bar dataKey="retry" stackId="a" fill="#f666a9" />
|
||||||
<Bar dataKey="archived" stackId="a" fill="#ac4776" />
|
<Bar dataKey="archived" stackId="a" fill="#ac4776" />
|
||||||
|
<Bar dataKey="completed" stackId="a" fill="#4bb543" />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
);
|
);
|
||||||
|
@ -50,6 +50,7 @@ enum SortBy {
|
|||||||
State,
|
State,
|
||||||
Size,
|
Size,
|
||||||
MemoryUsage,
|
MemoryUsage,
|
||||||
|
Latency,
|
||||||
Processed,
|
Processed,
|
||||||
Failed,
|
Failed,
|
||||||
ErrorRate,
|
ErrorRate,
|
||||||
@ -72,6 +73,12 @@ const colConfigs: SortableTableColumn<SortBy>[] = [
|
|||||||
sortBy: SortBy.MemoryUsage,
|
sortBy: SortBy.MemoryUsage,
|
||||||
align: "right",
|
align: "right",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Latency",
|
||||||
|
key: "latency",
|
||||||
|
sortBy: SortBy.Latency,
|
||||||
|
align: "right",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Processed",
|
label: "Processed",
|
||||||
key: "processed",
|
key: "processed",
|
||||||
@ -137,6 +144,10 @@ export default function QueuesOverviewTable(props: Props) {
|
|||||||
if (q1.memory_usage_bytes === q2.memory_usage_bytes) return 0;
|
if (q1.memory_usage_bytes === q2.memory_usage_bytes) return 0;
|
||||||
isQ1Smaller = q1.memory_usage_bytes < q2.memory_usage_bytes;
|
isQ1Smaller = q1.memory_usage_bytes < q2.memory_usage_bytes;
|
||||||
break;
|
break;
|
||||||
|
case SortBy.Latency:
|
||||||
|
if (q1.latency_msec === q2.latency_msec) return 0;
|
||||||
|
isQ1Smaller = q1.latency_msec < q2.latency_msec;
|
||||||
|
break;
|
||||||
case SortBy.Processed:
|
case SortBy.Processed:
|
||||||
if (q1.processed === q2.processed) return 0;
|
if (q1.processed === q2.processed) return 0;
|
||||||
isQ1Smaller = q1.processed < q2.processed;
|
isQ1Smaller = q1.processed < q2.processed;
|
||||||
@ -172,25 +183,30 @@ export default function QueuesOverviewTable(props: Props) {
|
|||||||
<Table className={classes.table} aria-label="queues overview table">
|
<Table className={classes.table} aria-label="queues overview table">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{colConfigs.map((cfg, i) => (
|
{colConfigs
|
||||||
<TableCell
|
.filter((cfg) => {
|
||||||
key={cfg.key}
|
// Filter out actions column in readonly mode.
|
||||||
align={cfg.align}
|
return !window.READ_ONLY || cfg.key !== "actions";
|
||||||
className={clsx(i === 0 && classes.fixedCell)}
|
})
|
||||||
>
|
.map((cfg, i) => (
|
||||||
{cfg.sortBy !== SortBy.None ? (
|
<TableCell
|
||||||
<TableSortLabel
|
key={cfg.key}
|
||||||
active={sortBy === cfg.sortBy}
|
align={cfg.align}
|
||||||
direction={sortDir}
|
className={clsx(i === 0 && classes.fixedCell)}
|
||||||
onClick={createSortClickHandler(cfg.sortBy)}
|
>
|
||||||
>
|
{cfg.sortBy !== SortBy.None ? (
|
||||||
{cfg.label}
|
<TableSortLabel
|
||||||
</TableSortLabel>
|
active={sortBy === cfg.sortBy}
|
||||||
) : (
|
direction={sortDir}
|
||||||
<div>{cfg.label}</div>
|
onClick={createSortClickHandler(cfg.sortBy)}
|
||||||
)}
|
>
|
||||||
</TableCell>
|
{cfg.label}
|
||||||
))}
|
</TableSortLabel>
|
||||||
|
) : (
|
||||||
|
<div>{cfg.label}</div>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@ -283,53 +299,56 @@ function Row(props: RowProps) {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">{q.size}</TableCell>
|
<TableCell align="right">{q.size}</TableCell>
|
||||||
<TableCell align="right">{prettyBytes(q.memory_usage_bytes)}</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.processed}</TableCell>
|
||||||
<TableCell align="right">{q.failed}</TableCell>
|
<TableCell align="right">{q.failed}</TableCell>
|
||||||
<TableCell align="right">{percentage(q.failed, q.processed)}</TableCell>
|
<TableCell align="right">{percentage(q.failed, q.processed)}</TableCell>
|
||||||
<TableCell
|
{!window.READ_ONLY && (
|
||||||
align="center"
|
<TableCell
|
||||||
onMouseEnter={() => setShowIcons(true)}
|
align="center"
|
||||||
onMouseLeave={() => setShowIcons(false)}
|
onMouseEnter={() => setShowIcons(true)}
|
||||||
>
|
onMouseLeave={() => setShowIcons(false)}
|
||||||
<div className={classes.actionIconsContainer}>
|
>
|
||||||
{showIcons ? (
|
<div className={classes.actionIconsContainer}>
|
||||||
<React.Fragment>
|
{showIcons ? (
|
||||||
{q.paused ? (
|
<React.Fragment>
|
||||||
<Tooltip title="Resume">
|
{q.paused ? (
|
||||||
<IconButton
|
<Tooltip title="Resume">
|
||||||
color="secondary"
|
<IconButton
|
||||||
onClick={props.onResumeClick}
|
color="secondary"
|
||||||
disabled={q.requestPending}
|
onClick={props.onResumeClick}
|
||||||
size="small"
|
disabled={q.requestPending}
|
||||||
>
|
size="small"
|
||||||
<PlayCircleFilledIcon fontSize="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>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
</React.Fragment>
|
||||||
<Tooltip title="Pause">
|
) : (
|
||||||
<IconButton
|
<IconButton size="small">
|
||||||
color="primary"
|
<MoreHorizIcon fontSize="small" />
|
||||||
onClick={props.onPauseClick}
|
</IconButton>
|
||||||
disabled={q.requestPending}
|
)}
|
||||||
size="small"
|
</div>
|
||||||
>
|
</TableCell>
|
||||||
<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>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 Checkbox from "@material-ui/core/Checkbox";
|
||||||
import IconButton from "@material-ui/core/IconButton";
|
import IconButton from "@material-ui/core/IconButton";
|
||||||
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
|
import TableCell from "@material-ui/core/TableCell";
|
||||||
import DeleteIcon from "@material-ui/icons/Delete";
|
import TableRow from "@material-ui/core/TableRow";
|
||||||
|
import Tooltip from "@material-ui/core/Tooltip";
|
||||||
import ArchiveIcon from "@material-ui/icons/Archive";
|
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 MoreHorizIcon from "@material-ui/icons/MoreHoriz";
|
||||||
import Alert from "@material-ui/lab/Alert";
|
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
|
||||||
import AlertTitle from "@material-ui/lab/AlertTitle";
|
import React from "react";
|
||||||
import SyntaxHighlighter from "./SyntaxHighlighter";
|
import { connect, ConnectedProps } from "react-redux";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { taskRowsPerPageChange } from "../actions/settingsActions";
|
||||||
import {
|
import {
|
||||||
|
archiveAllRetryTasksAsync,
|
||||||
|
archiveRetryTaskAsync,
|
||||||
|
batchArchiveRetryTasksAsync,
|
||||||
batchDeleteRetryTasksAsync,
|
batchDeleteRetryTasksAsync,
|
||||||
batchRunRetryTasksAsync,
|
batchRunRetryTasksAsync,
|
||||||
batchArchiveRetryTasksAsync,
|
|
||||||
deleteAllRetryTasksAsync,
|
deleteAllRetryTasksAsync,
|
||||||
runAllRetryTasksAsync,
|
|
||||||
archiveAllRetryTasksAsync,
|
|
||||||
listRetryTasksAsync,
|
|
||||||
deleteRetryTaskAsync,
|
deleteRetryTaskAsync,
|
||||||
|
listRetryTasksAsync,
|
||||||
|
runAllRetryTasksAsync,
|
||||||
runRetryTaskAsync,
|
runRetryTaskAsync,
|
||||||
archiveRetryTaskAsync,
|
|
||||||
} from "../actions/tasksActions";
|
} from "../actions/tasksActions";
|
||||||
import { AppState } from "../store";
|
import TasksTable, { RowProps, useRowStyles } from "./TasksTable";
|
||||||
import TablePaginationActions, {
|
|
||||||
rowsPerPageOptions,
|
|
||||||
} from "./TablePaginationActions";
|
|
||||||
import { taskRowsPerPageChange } from "../actions/settingsActions";
|
|
||||||
import TableActions from "./TableActions";
|
|
||||||
import { durationBefore, uuidPrefix } from "../utils";
|
|
||||||
import { usePolling } from "../hooks";
|
|
||||||
import { RetryTaskExtended } from "../reducers/tasksReducer";
|
|
||||||
import { TableColumn } from "../types/table";
|
|
||||||
import { taskDetailsPath } from "../paths";
|
import { taskDetailsPath } from "../paths";
|
||||||
|
import { AppState } from "../store";
|
||||||
const useStyles = makeStyles((theme) => ({
|
import { TableColumn } from "../types/table";
|
||||||
table: {
|
import { durationBefore, prettifyPayload, uuidPrefix } from "../utils";
|
||||||
minWidth: 650,
|
import SyntaxHighlighter from "./SyntaxHighlighter";
|
||||||
},
|
|
||||||
stickyHeaderCell: {
|
|
||||||
background: theme.palette.background.paper,
|
|
||||||
},
|
|
||||||
alert: {
|
|
||||||
borderTopLeftRadius: 0,
|
|
||||||
borderTopRightRadius: 0,
|
|
||||||
},
|
|
||||||
pagination: {
|
|
||||||
border: "none",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
function mapStateToProps(state: AppState) {
|
function mapStateToProps(state: AppState) {
|
||||||
return {
|
return {
|
||||||
@ -74,16 +44,16 @@ function mapStateToProps(state: AppState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
batchDeleteRetryTasksAsync,
|
batchDeleteTasks: batchDeleteRetryTasksAsync,
|
||||||
batchRunRetryTasksAsync,
|
batchRunTasks: batchRunRetryTasksAsync,
|
||||||
batchArchiveRetryTasksAsync,
|
batchArchiveTasks: batchArchiveRetryTasksAsync,
|
||||||
deleteAllRetryTasksAsync,
|
deleteAllTasks: deleteAllRetryTasksAsync,
|
||||||
runAllRetryTasksAsync,
|
runAllTasks: runAllRetryTasksAsync,
|
||||||
archiveAllRetryTasksAsync,
|
archiveAllTasks: archiveAllRetryTasksAsync,
|
||||||
listRetryTasksAsync,
|
listTasks: listRetryTasksAsync,
|
||||||
deleteRetryTaskAsync,
|
deleteTask: deleteRetryTaskAsync,
|
||||||
runRetryTaskAsync,
|
runTask: runRetryTaskAsync,
|
||||||
archiveRetryTaskAsync,
|
archiveTask: archiveRetryTaskAsync,
|
||||||
taskRowsPerPageChange,
|
taskRowsPerPageChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -96,265 +66,16 @@ interface Props {
|
|||||||
totalTaskCount: number; // totoal number of scheduled tasks.
|
totalTaskCount: number; // totoal number of scheduled tasks.
|
||||||
}
|
}
|
||||||
|
|
||||||
function RetryTasksTable(props: Props & ReduxProps) {
|
const columns: TableColumn[] = [
|
||||||
const { pollInterval, listRetryTasksAsync, queue, pageSize } = props;
|
{ key: "id", label: "ID", align: "left" },
|
||||||
const classes = useStyles();
|
{ key: "type", label: "Type", align: "left" },
|
||||||
const [page, setPage] = useState(0);
|
{ key: "payload", label: "Payload", align: "left" },
|
||||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
{ key: "retry_in", label: "Retry In", align: "left" },
|
||||||
const [activeTaskId, setActiveTaskId] = useState<string>("");
|
{ key: "last_error", label: "Last Error", align: "left" },
|
||||||
|
{ key: "retried", label: "Retried", align: "right" },
|
||||||
const handleChangePage = (
|
{ key: "max_retry", label: "Max Retry", align: "right" },
|
||||||
event: React.MouseEvent<HTMLButtonElement> | null,
|
{ key: "actions", label: "Actions", align: "center" },
|
||||||
newPage: number
|
];
|
||||||
) => {
|
|
||||||
setPage(newPage);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeRowsPerPage = (
|
|
||||||
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,
|
|
||||||
}}
|
|
||||||
onChangePage={handleChangePage}
|
|
||||||
onChangeRowsPerPage={handleChangeRowsPerPage}
|
|
||||||
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: RetryTaskExtended;
|
|
||||||
isSelected: boolean;
|
|
||||||
onSelectChange: (checked: boolean) => void;
|
|
||||||
onDeleteClick: () => void;
|
|
||||||
onRunClick: () => void;
|
|
||||||
onArchiveClick: () => void;
|
|
||||||
allActionPending: boolean;
|
|
||||||
showActions: boolean;
|
|
||||||
onActionCellEnter: () => void;
|
|
||||||
onActionCellLeave: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Row(props: RowProps) {
|
function Row(props: RowProps) {
|
||||||
const { task } = props;
|
const { task } = props;
|
||||||
@ -368,18 +89,34 @@ function Row(props: RowProps) {
|
|||||||
selected={props.isSelected}
|
selected={props.isSelected}
|
||||||
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
||||||
>
|
>
|
||||||
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
{!window.READ_ONLY && (
|
||||||
<IconButton>
|
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
||||||
<Checkbox
|
<IconButton>
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
<Checkbox
|
||||||
props.onSelectChange(event.target.checked)
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
}
|
props.onSelectChange(event.target.checked)
|
||||||
checked={props.isSelected}
|
}
|
||||||
/>
|
checked={props.isSelected}
|
||||||
</IconButton>
|
/>
|
||||||
</TableCell>
|
</IconButton>
|
||||||
<TableCell component="th" scope="row">
|
</TableCell>
|
||||||
{uuidPrefix(task.id)}
|
)}
|
||||||
|
<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>
|
||||||
<TableCell>{task.type}</TableCell>
|
<TableCell>{task.type}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@ -387,61 +124,74 @@ function Row(props: RowProps) {
|
|||||||
language="json"
|
language="json"
|
||||||
customStyle={{ margin: 0, maxWidth: 400 }}
|
customStyle={{ margin: 0, maxWidth: 400 }}
|
||||||
>
|
>
|
||||||
{task.payload}
|
{prettifyPayload(task.payload)}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{durationBefore(task.next_process_at)}</TableCell>
|
<TableCell>{durationBefore(task.next_process_at)}</TableCell>
|
||||||
<TableCell>{task.error_message}</TableCell>
|
<TableCell>{task.error_message}</TableCell>
|
||||||
<TableCell align="right">{task.retried}</TableCell>
|
<TableCell align="right">{task.retried}</TableCell>
|
||||||
<TableCell align="right">{task.max_retry}</TableCell>
|
<TableCell align="right">{task.max_retry}</TableCell>
|
||||||
<TableCell
|
{!window.READ_ONLY && (
|
||||||
align="center"
|
<TableCell
|
||||||
className={classes.actionCell}
|
align="center"
|
||||||
onMouseEnter={props.onActionCellEnter}
|
className={classes.actionCell}
|
||||||
onMouseLeave={props.onActionCellLeave}
|
onMouseEnter={props.onActionCellEnter}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onMouseLeave={props.onActionCellLeave}
|
||||||
>
|
onClick={(e) => e.stopPropagation()}
|
||||||
{props.showActions ? (
|
>
|
||||||
<React.Fragment>
|
{props.showActions ? (
|
||||||
<Tooltip title="Delete">
|
<React.Fragment>
|
||||||
<IconButton
|
<Tooltip title="Delete">
|
||||||
onClick={props.onDeleteClick}
|
<IconButton
|
||||||
disabled={task.requestPending || props.allActionPending}
|
onClick={props.onDeleteClick}
|
||||||
size="small"
|
disabled={task.requestPending || props.allActionPending}
|
||||||
className={classes.actionButton}
|
size="small"
|
||||||
>
|
className={classes.actionButton}
|
||||||
<DeleteIcon fontSize="small" />
|
>
|
||||||
</IconButton>
|
<DeleteIcon fontSize="small" />
|
||||||
</Tooltip>
|
</IconButton>
|
||||||
<Tooltip title="Archive">
|
</Tooltip>
|
||||||
<IconButton
|
<Tooltip title="Archive">
|
||||||
onClick={props.onArchiveClick}
|
<IconButton
|
||||||
disabled={task.requestPending || props.allActionPending}
|
onClick={props.onArchiveClick}
|
||||||
size="small"
|
disabled={task.requestPending || props.allActionPending}
|
||||||
className={classes.actionButton}
|
size="small"
|
||||||
>
|
className={classes.actionButton}
|
||||||
<ArchiveIcon fontSize="small" />
|
>
|
||||||
</IconButton>
|
<ArchiveIcon fontSize="small" />
|
||||||
</Tooltip>
|
</IconButton>
|
||||||
<Tooltip title="Run">
|
</Tooltip>
|
||||||
<IconButton
|
<Tooltip title="Run">
|
||||||
onClick={props.onRunClick}
|
<IconButton
|
||||||
disabled={task.requestPending || props.allActionPending}
|
onClick={props.onRunClick}
|
||||||
size="small"
|
disabled={task.requestPending || props.allActionPending}
|
||||||
className={classes.actionButton}
|
size="small"
|
||||||
>
|
className={classes.actionButton}
|
||||||
<PlayArrowIcon fontSize="small" />
|
>
|
||||||
</IconButton>
|
<PlayArrowIcon fontSize="small" />
|
||||||
</Tooltip>
|
</IconButton>
|
||||||
</React.Fragment>
|
</Tooltip>
|
||||||
) : (
|
</React.Fragment>
|
||||||
<IconButton size="small" onClick={props.onActionCellEnter}>
|
) : (
|
||||||
<MoreHorizIcon fontSize="small" />
|
<IconButton size="small" onClick={props.onActionCellEnter}>
|
||||||
</IconButton>
|
<MoreHorizIcon fontSize="small" />
|
||||||
)}
|
</IconButton>
|
||||||
</TableCell>
|
)}
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RetryTasksTable(props: Props & ReduxProps) {
|
||||||
|
return (
|
||||||
|
<TasksTable
|
||||||
|
taskState="retry"
|
||||||
|
columns={columns}
|
||||||
|
renderRow={(rowProps: RowProps) => <Row {...rowProps} />}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default connector(RetryTasksTable);
|
export default connector(RetryTasksTable);
|
||||||
|
@ -1,26 +1,18 @@
|
|||||||
import React, { useState, useCallback } from "react";
|
import React from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
|
||||||
import { connect, ConnectedProps } from "react-redux";
|
import { connect, ConnectedProps } from "react-redux";
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { useHistory } from "react-router-dom";
|
||||||
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 TableRow from "@material-ui/core/TableRow";
|
||||||
import TableFooter from "@material-ui/core/TableFooter";
|
import TableCell from "@material-ui/core/TableCell";
|
||||||
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 Checkbox from "@material-ui/core/Checkbox";
|
||||||
import IconButton from "@material-ui/core/IconButton";
|
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 DeleteIcon from "@material-ui/icons/Delete";
|
||||||
import ArchiveIcon from "@material-ui/icons/Archive";
|
import ArchiveIcon from "@material-ui/icons/Archive";
|
||||||
import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
|
import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
|
||||||
import Alert from "@material-ui/lab/Alert";
|
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
|
||||||
import AlertTitle from "@material-ui/lab/AlertTitle";
|
|
||||||
import SyntaxHighlighter from "./SyntaxHighlighter";
|
import SyntaxHighlighter from "./SyntaxHighlighter";
|
||||||
|
import TasksTable, { RowProps, useRowStyles } from "./TasksTable";
|
||||||
import {
|
import {
|
||||||
batchDeleteScheduledTasksAsync,
|
batchDeleteScheduledTasksAsync,
|
||||||
batchRunScheduledTasksAsync,
|
batchRunScheduledTasksAsync,
|
||||||
@ -35,32 +27,10 @@ import {
|
|||||||
} from "../actions/tasksActions";
|
} from "../actions/tasksActions";
|
||||||
import { taskRowsPerPageChange } from "../actions/settingsActions";
|
import { taskRowsPerPageChange } from "../actions/settingsActions";
|
||||||
import { AppState } from "../store";
|
import { AppState } from "../store";
|
||||||
import TablePaginationActions, {
|
|
||||||
rowsPerPageOptions,
|
|
||||||
} from "./TablePaginationActions";
|
|
||||||
import TableActions from "./TableActions";
|
|
||||||
import { durationBefore, uuidPrefix } from "../utils";
|
|
||||||
import { usePolling } from "../hooks";
|
|
||||||
import { ScheduledTaskExtended } from "../reducers/tasksReducer";
|
|
||||||
import { TableColumn } from "../types/table";
|
import { TableColumn } from "../types/table";
|
||||||
|
import { durationBefore, prettifyPayload, uuidPrefix } from "../utils";
|
||||||
import { taskDetailsPath } from "../paths";
|
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) {
|
function mapStateToProps(state: AppState) {
|
||||||
return {
|
return {
|
||||||
loading: state.tasks.scheduledTasks.loading,
|
loading: state.tasks.scheduledTasks.loading,
|
||||||
@ -74,16 +44,16 @@ function mapStateToProps(state: AppState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
listScheduledTasksAsync,
|
listTasks: listScheduledTasksAsync,
|
||||||
batchDeleteScheduledTasksAsync,
|
batchDeleteTasks: batchDeleteScheduledTasksAsync,
|
||||||
batchRunScheduledTasksAsync,
|
batchRunTasks: batchRunScheduledTasksAsync,
|
||||||
batchArchiveScheduledTasksAsync,
|
batchArchiveTasks: batchArchiveScheduledTasksAsync,
|
||||||
deleteAllScheduledTasksAsync,
|
deleteAllTasks: deleteAllScheduledTasksAsync,
|
||||||
runAllScheduledTasksAsync,
|
runAllTasks: runAllScheduledTasksAsync,
|
||||||
archiveAllScheduledTasksAsync,
|
archiveAllTasks: archiveAllScheduledTasksAsync,
|
||||||
deleteScheduledTaskAsync,
|
deleteTask: deleteScheduledTaskAsync,
|
||||||
runScheduledTaskAsync,
|
runTask: runScheduledTaskAsync,
|
||||||
archiveScheduledTaskAsync,
|
archiveTask: archiveScheduledTaskAsync,
|
||||||
taskRowsPerPageChange,
|
taskRowsPerPageChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -96,262 +66,13 @@ interface Props {
|
|||||||
totalTaskCount: number; // totoal number of scheduled tasks.
|
totalTaskCount: number; // totoal number of scheduled tasks.
|
||||||
}
|
}
|
||||||
|
|
||||||
function ScheduledTasksTable(props: Props & ReduxProps) {
|
const columns: TableColumn[] = [
|
||||||
const { pollInterval, listScheduledTasksAsync, queue, pageSize } = props;
|
{ key: "id", label: "ID", align: "left" },
|
||||||
const classes = useStyles();
|
{ key: "type", label: "Type", align: "left" },
|
||||||
const [page, setPage] = useState(0);
|
{ key: "payload", label: "Payload", align: "left" },
|
||||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
{ key: "process_in", label: "Process In", align: "left" },
|
||||||
const [activeTaskId, setActiveTaskId] = useState<string>("");
|
{ key: "actions", label: "Actions", align: "center" },
|
||||||
|
];
|
||||||
const handleChangePage = (
|
|
||||||
event: React.MouseEvent<HTMLButtonElement> | null,
|
|
||||||
newPage: number
|
|
||||||
) => {
|
|
||||||
setPage(newPage);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeRowsPerPage = (
|
|
||||||
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,
|
|
||||||
}}
|
|
||||||
onChangePage={handleChangePage}
|
|
||||||
onChangeRowsPerPage={handleChangeRowsPerPage}
|
|
||||||
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: ScheduledTaskExtended;
|
|
||||||
isSelected: boolean;
|
|
||||||
onSelectChange: (checked: boolean) => void;
|
|
||||||
onRunClick: () => void;
|
|
||||||
onDeleteClick: () => void;
|
|
||||||
onArchiveClick: () => void;
|
|
||||||
allActionPending: boolean;
|
|
||||||
showActions: boolean;
|
|
||||||
onActionCellEnter: () => void;
|
|
||||||
onActionCellLeave: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Row(props: RowProps) {
|
function Row(props: RowProps) {
|
||||||
const { task } = props;
|
const { task } = props;
|
||||||
@ -364,18 +85,34 @@ function Row(props: RowProps) {
|
|||||||
selected={props.isSelected}
|
selected={props.isSelected}
|
||||||
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
||||||
>
|
>
|
||||||
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
{!window.READ_ONLY && (
|
||||||
<IconButton>
|
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
||||||
<Checkbox
|
<IconButton>
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
<Checkbox
|
||||||
props.onSelectChange(event.target.checked)
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
}
|
props.onSelectChange(event.target.checked)
|
||||||
checked={props.isSelected}
|
}
|
||||||
/>
|
checked={props.isSelected}
|
||||||
</IconButton>
|
/>
|
||||||
</TableCell>
|
</IconButton>
|
||||||
<TableCell component="th" scope="row">
|
</TableCell>
|
||||||
{uuidPrefix(task.id)}
|
)}
|
||||||
|
<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>
|
||||||
<TableCell>{task.type}</TableCell>
|
<TableCell>{task.type}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@ -383,57 +120,71 @@ function Row(props: RowProps) {
|
|||||||
language="json"
|
language="json"
|
||||||
customStyle={{ margin: 0, maxWidth: 400 }}
|
customStyle={{ margin: 0, maxWidth: 400 }}
|
||||||
>
|
>
|
||||||
{task.payload}
|
{prettifyPayload(task.payload)}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{durationBefore(task.next_process_at)}</TableCell>
|
<TableCell>{durationBefore(task.next_process_at)}</TableCell>
|
||||||
<TableCell
|
{!window.READ_ONLY && (
|
||||||
align="center"
|
<TableCell
|
||||||
className={classes.actionCell}
|
align="center"
|
||||||
onMouseEnter={props.onActionCellEnter}
|
className={classes.actionCell}
|
||||||
onMouseLeave={props.onActionCellLeave}
|
onMouseEnter={props.onActionCellEnter}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onMouseLeave={props.onActionCellLeave}
|
||||||
>
|
onClick={(e) => e.stopPropagation()}
|
||||||
{props.showActions ? (
|
>
|
||||||
<React.Fragment>
|
{props.showActions ? (
|
||||||
<Tooltip title="Delete">
|
<React.Fragment>
|
||||||
<IconButton
|
<Tooltip title="Delete">
|
||||||
onClick={props.onDeleteClick}
|
<IconButton
|
||||||
disabled={task.requestPending || props.allActionPending}
|
onClick={props.onDeleteClick}
|
||||||
size="small"
|
disabled={task.requestPending || props.allActionPending}
|
||||||
className={classes.actionButton}
|
size="small"
|
||||||
>
|
className={classes.actionButton}
|
||||||
<DeleteIcon fontSize="small" />
|
>
|
||||||
</IconButton>
|
<DeleteIcon fontSize="small" />
|
||||||
</Tooltip>
|
</IconButton>
|
||||||
<Tooltip title="Archive">
|
</Tooltip>
|
||||||
<IconButton
|
<Tooltip title="Archive">
|
||||||
onClick={props.onArchiveClick}
|
<IconButton
|
||||||
disabled={task.requestPending || props.allActionPending}
|
onClick={props.onArchiveClick}
|
||||||
size="small"
|
disabled={task.requestPending || props.allActionPending}
|
||||||
className={classes.actionButton}
|
size="small"
|
||||||
>
|
className={classes.actionButton}
|
||||||
<ArchiveIcon fontSize="small" />
|
>
|
||||||
</IconButton>
|
<ArchiveIcon fontSize="small" />
|
||||||
</Tooltip>
|
</IconButton>
|
||||||
<Tooltip title="Run">
|
</Tooltip>
|
||||||
<IconButton
|
<Tooltip title="Run">
|
||||||
onClick={props.onRunClick}
|
<IconButton
|
||||||
disabled={task.requestPending || props.allActionPending}
|
onClick={props.onRunClick}
|
||||||
size="small"
|
disabled={task.requestPending || props.allActionPending}
|
||||||
className={classes.actionButton}
|
size="small"
|
||||||
>
|
className={classes.actionButton}
|
||||||
<PlayArrowIcon fontSize="small" />
|
>
|
||||||
</IconButton>
|
<PlayArrowIcon fontSize="small" />
|
||||||
</Tooltip>
|
</IconButton>
|
||||||
</React.Fragment>
|
</Tooltip>
|
||||||
) : (
|
</React.Fragment>
|
||||||
<IconButton size="small" onClick={props.onActionCellEnter}>
|
) : (
|
||||||
<MoreHorizIcon fontSize="small" />
|
<IconButton size="small" onClick={props.onActionCellEnter}>
|
||||||
</IconButton>
|
<MoreHorizIcon fontSize="small" />
|
||||||
)}
|
</IconButton>
|
||||||
</TableCell>
|
)}
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ScheduledTasksTable(props: Props & ReduxProps) {
|
||||||
|
return (
|
||||||
|
<TasksTable
|
||||||
|
taskState="scheduled"
|
||||||
|
columns={columns}
|
||||||
|
renderRow={(rowProps: RowProps) => <Row {...rowProps} />}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default connector(ScheduledTasksTable);
|
export default connector(ScheduledTasksTable);
|
||||||
|
@ -18,7 +18,7 @@ import { SortDirection, SortableTableColumn } from "../types/table";
|
|||||||
import TableSortLabel from "@material-ui/core/TableSortLabel";
|
import TableSortLabel from "@material-ui/core/TableSortLabel";
|
||||||
import SyntaxHighlighter from "./SyntaxHighlighter";
|
import SyntaxHighlighter from "./SyntaxHighlighter";
|
||||||
import { SchedulerEntry } from "../api";
|
import { SchedulerEntry } from "../api";
|
||||||
import { timeAgo, durationBefore } from "../utils";
|
import { timeAgo, durationBefore, prettifyPayload } from "../utils";
|
||||||
import SchedulerEnqueueEventsTable from "./SchedulerEnqueueEventsTable";
|
import SchedulerEnqueueEventsTable from "./SchedulerEnqueueEventsTable";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
@ -288,7 +288,7 @@ function Row(props: RowProps) {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className={clsx(isLastRow && classes.noBorder)}>
|
<TableCell className={clsx(isLastRow && classes.noBorder)}>
|
||||||
<SyntaxHighlighter language="json">
|
<SyntaxHighlighter language="json">
|
||||||
{entry.task_payload}
|
{prettifyPayload(entry.task_payload)}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className={clsx(isLastRow && classes.noBorder)}>
|
<TableCell className={clsx(isLastRow && classes.noBorder)}>
|
||||||
|
@ -21,7 +21,7 @@ import AlertTitle from "@material-ui/lab/AlertTitle";
|
|||||||
import SyntaxHighlighter from "./SyntaxHighlighter";
|
import SyntaxHighlighter from "./SyntaxHighlighter";
|
||||||
import { ServerInfo } from "../api";
|
import { ServerInfo } from "../api";
|
||||||
import { SortDirection, SortableTableColumn } from "../types/table";
|
import { SortDirection, SortableTableColumn } from "../types/table";
|
||||||
import { timeAgo, uuidPrefix } from "../utils";
|
import { timeAgo, uuidPrefix, prettifyPayload } from "../utils";
|
||||||
import { queueDetailsPath } from "../paths";
|
import { queueDetailsPath } from "../paths";
|
||||||
import Typography from "@material-ui/core/Typography";
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
|
||||||
@ -282,7 +282,7 @@ function Row(props: RowProps) {
|
|||||||
language="json"
|
language="json"
|
||||||
customStyle={{ margin: 0 }}
|
customStyle={{ margin: 0 }}
|
||||||
>
|
>
|
||||||
{worker.task_payload}
|
{prettifyPayload(worker.task_payload)}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{worker.queue}</TableCell>
|
<TableCell>{worker.queue}</TableCell>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTheme, Theme } from "@material-ui/core/styles";
|
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 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 styleLight from "react-syntax-highlighter/dist/esm/styles/hljs/atom-one-light";
|
||||||
import { isDarkTheme } from "../theme";
|
import { isDarkTheme } from "../theme";
|
||||||
@ -11,6 +12,8 @@ interface Props {
|
|||||||
customStyle?: object;
|
customStyle?: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ReactSyntaxHighlighter.registerLanguage("json", json);
|
||||||
|
|
||||||
// Theme aware syntax-highlighter component.
|
// Theme aware syntax-highlighter component.
|
||||||
export default function SyntaxHighlighter(props: Props) {
|
export default function SyntaxHighlighter(props: Props) {
|
||||||
const theme = useTheme<Theme>();
|
const theme = useTheme<Theme>();
|
||||||
|
@ -24,7 +24,7 @@ interface TablePaginationActionsProps {
|
|||||||
count: number;
|
count: number;
|
||||||
page: number;
|
page: number;
|
||||||
rowsPerPage: number;
|
rowsPerPage: number;
|
||||||
onChangePage: (
|
onPageChange: (
|
||||||
event: React.MouseEvent<HTMLButtonElement>,
|
event: React.MouseEvent<HTMLButtonElement>,
|
||||||
newPage: number
|
newPage: number
|
||||||
) => void;
|
) => void;
|
||||||
@ -33,30 +33,30 @@ interface TablePaginationActionsProps {
|
|||||||
function TablePaginationActions(props: TablePaginationActionsProps) {
|
function TablePaginationActions(props: TablePaginationActionsProps) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { count, page, rowsPerPage, onChangePage } = props;
|
const { count, page, rowsPerPage, onPageChange } = props;
|
||||||
|
|
||||||
const handleFirstPageButtonClick = (
|
const handleFirstPageButtonClick = (
|
||||||
event: React.MouseEvent<HTMLButtonElement>
|
event: React.MouseEvent<HTMLButtonElement>
|
||||||
) => {
|
) => {
|
||||||
onChangePage(event, 0);
|
onPageChange(event, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBackButtonClick = (
|
const handleBackButtonClick = (
|
||||||
event: React.MouseEvent<HTMLButtonElement>
|
event: React.MouseEvent<HTMLButtonElement>
|
||||||
) => {
|
) => {
|
||||||
onChangePage(event, page - 1);
|
onPageChange(event, page - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNextButtonClick = (
|
const handleNextButtonClick = (
|
||||||
event: React.MouseEvent<HTMLButtonElement>
|
event: React.MouseEvent<HTMLButtonElement>
|
||||||
) => {
|
) => {
|
||||||
onChangePage(event, page + 1);
|
onPageChange(event, page + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLastPageButtonClick = (
|
const handleLastPageButtonClick = (
|
||||||
event: React.MouseEvent<HTMLButtonElement>
|
event: React.MouseEvent<HTMLButtonElement>
|
||||||
) => {
|
) => {
|
||||||
onChangePage(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
|
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,236 +1,378 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useCallback } from "react";
|
||||||
import { connect, ConnectedProps } from "react-redux";
|
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
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 Paper from "@material-ui/core/Paper";
|
||||||
import Chip from "@material-ui/core/Chip";
|
import Checkbox from "@material-ui/core/Checkbox";
|
||||||
import InputBase from "@material-ui/core/InputBase";
|
import IconButton from "@material-ui/core/IconButton";
|
||||||
import SearchIcon from "@material-ui/icons/Search";
|
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
|
||||||
import ActiveTasksTable from "./ActiveTasksTable";
|
import DeleteIcon from "@material-ui/icons/Delete";
|
||||||
import PendingTasksTable from "./PendingTasksTable";
|
import ArchiveIcon from "@material-ui/icons/Archive";
|
||||||
import ScheduledTasksTable from "./ScheduledTasksTable";
|
import CancelIcon from "@material-ui/icons/Cancel";
|
||||||
import RetryTasksTable from "./RetryTasksTable";
|
import Alert from "@material-ui/lab/Alert";
|
||||||
import ArchivedTasksTable from "./ArchivedTasksTable";
|
import AlertTitle from "@material-ui/lab/AlertTitle";
|
||||||
import { useHistory } from "react-router-dom";
|
import TablePaginationActions, {
|
||||||
import { queueDetailsPath, taskDetailsPath } from "../paths";
|
rowsPerPageOptions,
|
||||||
import { QueueInfo } from "../reducers/queuesReducer";
|
} from "./TablePaginationActions";
|
||||||
import { AppState } from "../store";
|
import TableActions from "./TableActions";
|
||||||
import { isDarkTheme } from "../theme";
|
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 {
|
const useStyles = makeStyles((theme) => ({
|
||||||
children?: React.ReactNode;
|
table: {
|
||||||
selected: string; // currently selected value
|
minWidth: 650,
|
||||||
value: string; // tab panel will be shown if selected value equals to the value
|
},
|
||||||
|
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) {
|
export default function TasksTable(props: Props) {
|
||||||
const { children, value, selected, ...other } = 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 (
|
return (
|
||||||
<div
|
<div>
|
||||||
role="tabpanel"
|
{!window.READ_ONLY && (
|
||||||
hidden={value !== selected}
|
<TableActions
|
||||||
id={`scrollable-auto-tabpanel-${selected}`}
|
showIconButtons={numSelected > 0}
|
||||||
aria-labelledby={`scrollable-auto-tab-${selected}`}
|
iconButtonActions={batchActions}
|
||||||
style={{ flex: 1, overflowY: "scroll" }}
|
menuItemActions={allActions}
|
||||||
{...other}
|
/>
|
||||||
>
|
)}
|
||||||
{value === selected && children}
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStatetoProps(state: AppState, ownProps: Props) {
|
export const useRowStyles = makeStyles((theme) => ({
|
||||||
// TODO: Add loading state for each queue.
|
root: {
|
||||||
const queueInfo = state.queues.data.find(
|
cursor: "pointer",
|
||||||
(q: QueueInfo) => q.name === ownProps.queue
|
"& #copy-button": {
|
||||||
);
|
display: "none",
|
||||||
const currentStats = queueInfo
|
},
|
||||||
? queueInfo.currentStats
|
"&:hover": {
|
||||||
: {
|
boxShadow: theme.shadows[2],
|
||||||
queue: ownProps.queue,
|
"& #copy-button": {
|
||||||
paused: false,
|
display: "inline-block",
|
||||||
size: 0,
|
},
|
||||||
active: 0,
|
},
|
||||||
pending: 0,
|
"&:hover $copyButton": {
|
||||||
scheduled: 0,
|
display: "inline-block",
|
||||||
retry: 0,
|
},
|
||||||
archived: 0,
|
"&:hover .MuiTableCell-root": {
|
||||||
processed: 0,
|
borderBottomColor: theme.palette.background.paper,
|
||||||
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: {
|
|
||||||
marginLeft: theme.spacing(4),
|
|
||||||
},
|
|
||||||
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],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
searchIcon: {
|
actionCell: {
|
||||||
padding: theme.spacing(0, 2),
|
width: "140px",
|
||||||
height: "100%",
|
},
|
||||||
position: "absolute",
|
actionButton: {
|
||||||
pointerEvents: "none",
|
marginLeft: 3,
|
||||||
|
marginRight: 3,
|
||||||
|
},
|
||||||
|
idCell: {
|
||||||
|
width: "200px",
|
||||||
|
},
|
||||||
|
copyButton: {
|
||||||
|
display: "none",
|
||||||
|
},
|
||||||
|
IdGroup: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
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) {
|
export interface RowProps {
|
||||||
const { currentStats } = props;
|
key: string;
|
||||||
const classes = useStyles();
|
task: TaskInfoExtended;
|
||||||
const history = useHistory();
|
isSelected: boolean;
|
||||||
const chips = [
|
onSelectChange: (checked: boolean) => void;
|
||||||
{ key: "active", label: "Active", count: currentStats.active },
|
onRunClick?: () => void;
|
||||||
{ key: "pending", label: "Pending", count: currentStats.pending },
|
onDeleteClick?: () => void;
|
||||||
{ key: "scheduled", label: "Scheduled", count: currentStats.scheduled },
|
onArchiveClick?: () => void;
|
||||||
{ key: "retry", label: "Retry", count: currentStats.retry },
|
onCancelClick?: () => void;
|
||||||
{ key: "archived", label: "Archived", count: currentStats.archived },
|
allActionPending: boolean;
|
||||||
];
|
showActions: boolean;
|
||||||
|
onActionCellEnter: () => void;
|
||||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
onActionCellLeave: () => void;
|
||||||
|
|
||||||
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>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connector(TasksTable);
|
|
||||||
|
262
ui/src/components/TasksTableContainer.tsx
Normal file
262
ui/src/components/TasksTableContainer.tsx
Normal 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);
|
13
ui/src/global.d.ts
vendored
13
ui/src/global.d.ts
vendored
@ -1,5 +1,18 @@
|
|||||||
interface Window {
|
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 URL path for asynqmon app.
|
||||||
// ROOT_PATH should not have the tailing slash.
|
// ROOT_PATH should not have the tailing slash.
|
||||||
ROOT_PATH: string;
|
ROOT_PATH: string;
|
||||||
|
|
||||||
|
// 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;
|
||||||
}
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
// usePolling repeatedly calls doFn with a fix time delay specified
|
// usePolling repeatedly calls doFn with a fix time delay specified
|
||||||
// by interval (in millisecond).
|
// by interval (in millisecond).
|
||||||
@ -9,3 +10,9 @@ export function usePolling(doFn: () => void, interval: number) {
|
|||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, [interval, doFn]);
|
}, [interval, doFn]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// useQuery gets the URL search params from the current URL.
|
||||||
|
export function useQuery(): URLSearchParams {
|
||||||
|
const { search } = useLocation();
|
||||||
|
return useMemo(() => new URLSearchParams(search), [search]);
|
||||||
|
}
|
||||||
|
@ -4,10 +4,13 @@ import CssBaseline from "@material-ui/core/CssBaseline";
|
|||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import store from "./store";
|
import store from "./store";
|
||||||
|
import parseFlagsUnderWindow from "./parseFlags";
|
||||||
import * as serviceWorker from "./serviceWorker";
|
import * as serviceWorker from "./serviceWorker";
|
||||||
import { saveState } from "./localStorage";
|
import { saveState } from "./localStorage";
|
||||||
import { SettingsState } from "./reducers/settingsReducer";
|
import { SettingsState } from "./reducers/settingsReducer";
|
||||||
|
|
||||||
|
parseFlagsUnderWindow();
|
||||||
|
|
||||||
let currentSettings: SettingsState | undefined = undefined;
|
let currentSettings: SettingsState | undefined = undefined;
|
||||||
store.subscribe(() => {
|
store.subscribe(() => {
|
||||||
const prevSettings = currentSettings;
|
const prevSettings = currentSettings;
|
||||||
|
43
ui/src/parseFlags.ts
Normal file
43
ui/src/parseFlags.ts
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
export const paths = {
|
export const paths = () => ({
|
||||||
HOME: `${window.ROOT_PATH}/`,
|
HOME: `${window.ROOT_PATH}/`,
|
||||||
SETTINGS: `${window.ROOT_PATH}/settings`,
|
SETTINGS: `${window.ROOT_PATH}/settings`,
|
||||||
SERVERS: `${window.ROOT_PATH}/servers`,
|
SERVERS: `${window.ROOT_PATH}/servers`,
|
||||||
@ -6,14 +6,15 @@ export const paths = {
|
|||||||
QUEUE_DETAILS: `${window.ROOT_PATH}/queues/:qname`,
|
QUEUE_DETAILS: `${window.ROOT_PATH}/queues/:qname`,
|
||||||
REDIS: `${window.ROOT_PATH}/redis`,
|
REDIS: `${window.ROOT_PATH}/redis`,
|
||||||
TASK_DETAILS: `${window.ROOT_PATH}/queues/:qname/tasks/:taskId`,
|
TASK_DETAILS: `${window.ROOT_PATH}/queues/:qname/tasks/:taskId`,
|
||||||
};
|
QUEUE_METRICS: `${window.ROOT_PATH}/q/metrics`,
|
||||||
|
});
|
||||||
|
|
||||||
/**************************************************************
|
/**************************************************************
|
||||||
Path Helper functions
|
Path Helper functions
|
||||||
**************************************************************/
|
**************************************************************/
|
||||||
|
|
||||||
export function queueDetailsPath(qname: string, taskStatus?: string): string {
|
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) {
|
if (taskStatus) {
|
||||||
return `${path}?status=${taskStatus}`;
|
return `${path}?status=${taskStatus}`;
|
||||||
}
|
}
|
||||||
@ -21,7 +22,9 @@ export function queueDetailsPath(qname: string, taskStatus?: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function taskDetailsPath(qname: string, taskId: 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**************************************************************
|
/**************************************************************
|
||||||
|
55
ui/src/reducers/groupsReducer.ts
Normal file
55
ui/src/reducers/groupsReducer.ts
Normal 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;
|
49
ui/src/reducers/metricsReducer.ts
Normal file
49
ui/src/reducers/metricsReducer.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
GET_METRICS_BEGIN,
|
||||||
|
GET_METRICS_ERROR,
|
||||||
|
GET_METRICS_SUCCESS,
|
||||||
|
MetricsActionTypes,
|
||||||
|
} from "../actions/metricsActions";
|
||||||
|
import { MetricsResponse } from "../api";
|
||||||
|
|
||||||
|
interface MetricsState {
|
||||||
|
loading: boolean;
|
||||||
|
error: string;
|
||||||
|
data: MetricsResponse | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: MetricsState = {
|
||||||
|
loading: false,
|
||||||
|
error: "",
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function metricsReducer(
|
||||||
|
state = initialState,
|
||||||
|
action: MetricsActionTypes
|
||||||
|
): MetricsState {
|
||||||
|
switch (action.type) {
|
||||||
|
case GET_METRICS_BEGIN:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
loading: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
case GET_METRICS_ERROR:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
loading: false,
|
||||||
|
error: action.error,
|
||||||
|
};
|
||||||
|
|
||||||
|
case GET_METRICS_SUCCESS:
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
error: "",
|
||||||
|
data: action.payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
@ -74,5 +74,11 @@ export default function queueStatsReducer(
|
|||||||
|
|
||||||
// Returns true if two timestamps are from the same date.
|
// Returns true if two timestamps are from the same date.
|
||||||
function isSameDate(ts1: string, ts2: string): boolean {
|
function isSameDate(ts1: string, ts2: string): boolean {
|
||||||
return new Date(ts1).toDateString() === new Date(ts2).toDateString();
|
const date1 = new Date(ts1);
|
||||||
|
const date2 = new Date(ts2);
|
||||||
|
return (
|
||||||
|
date1.getUTCDate() === date2.getUTCDate() &&
|
||||||
|
date1.getUTCMonth() === date2.getUTCMonth() &&
|
||||||
|
date1.getUTCFullYear() === date2.getUTCFullYear()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
GroupsActionTypes,
|
||||||
|
LIST_GROUPS_SUCCESS,
|
||||||
|
} from "../actions/groupsActions";
|
||||||
import {
|
import {
|
||||||
LIST_QUEUES_SUCCESS,
|
LIST_QUEUES_SUCCESS,
|
||||||
LIST_QUEUES_BEGIN,
|
LIST_QUEUES_BEGIN,
|
||||||
@ -50,6 +54,19 @@ import {
|
|||||||
BATCH_DELETE_PENDING_TASKS_SUCCESS,
|
BATCH_DELETE_PENDING_TASKS_SUCCESS,
|
||||||
ARCHIVE_ALL_PENDING_TASKS_SUCCESS,
|
ARCHIVE_ALL_PENDING_TASKS_SUCCESS,
|
||||||
DELETE_ALL_PENDING_TASKS_SUCCESS,
|
DELETE_ALL_PENDING_TASKS_SUCCESS,
|
||||||
|
DELETE_COMPLETED_TASK_SUCCESS,
|
||||||
|
DELETE_ALL_COMPLETED_TASKS_SUCCESS,
|
||||||
|
BATCH_DELETE_COMPLETED_TASKS_SUCCESS,
|
||||||
|
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";
|
} from "../actions/tasksActions";
|
||||||
import { Queue } from "../api";
|
import { Queue } from "../api";
|
||||||
|
|
||||||
@ -69,7 +86,7 @@ const initialState: QueuesState = { data: [], loading: false, error: "" };
|
|||||||
|
|
||||||
function queuesReducer(
|
function queuesReducer(
|
||||||
state = initialState,
|
state = initialState,
|
||||||
action: QueuesActionTypes | TasksActionTypes
|
action: QueuesActionTypes | TasksActionTypes | GroupsActionTypes
|
||||||
): QueuesState {
|
): QueuesState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case LIST_QUEUES_BEGIN:
|
case LIST_QUEUES_BEGIN:
|
||||||
@ -159,6 +176,7 @@ function queuesReducer(
|
|||||||
|
|
||||||
case LIST_ACTIVE_TASKS_SUCCESS:
|
case LIST_ACTIVE_TASKS_SUCCESS:
|
||||||
case LIST_PENDING_TASKS_SUCCESS:
|
case LIST_PENDING_TASKS_SUCCESS:
|
||||||
|
case LIST_AGGREGATING_TASKS_SUCCESS:
|
||||||
case LIST_SCHEDULED_TASKS_SUCCESS:
|
case LIST_SCHEDULED_TASKS_SUCCESS:
|
||||||
case LIST_RETRY_TASKS_SUCCESS:
|
case LIST_RETRY_TASKS_SUCCESS:
|
||||||
case LIST_ARCHIVED_TASKS_SUCCESS: {
|
case LIST_ARCHIVED_TASKS_SUCCESS: {
|
||||||
@ -172,6 +190,23 @@ function queuesReducer(
|
|||||||
return { ...state, data: newData };
|
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: {
|
case RUN_SCHEDULED_TASK_SUCCESS: {
|
||||||
const newData = state.data.map((queueInfo) => {
|
const newData = state.data.map((queueInfo) => {
|
||||||
if (queueInfo.name !== action.queue) {
|
if (queueInfo.name !== action.queue) {
|
||||||
@ -240,6 +275,23 @@ function queuesReducer(
|
|||||||
return { ...state, data: newData };
|
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: {
|
case ARCHIVE_SCHEDULED_TASK_SUCCESS: {
|
||||||
const newData = state.data.map((queueInfo) => {
|
const newData = state.data.map((queueInfo) => {
|
||||||
if (queueInfo.name !== action.queue) {
|
if (queueInfo.name !== action.queue) {
|
||||||
@ -308,6 +360,23 @@ function queuesReducer(
|
|||||||
return { ...state, data: newData };
|
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: {
|
case BATCH_ARCHIVE_PENDING_TASKS_SUCCESS: {
|
||||||
const newData = state.data.map((queueInfo) => {
|
const newData = state.data.map((queueInfo) => {
|
||||||
if (queueInfo.name !== action.queue) {
|
if (queueInfo.name !== action.queue) {
|
||||||
@ -367,6 +436,24 @@ function queuesReducer(
|
|||||||
return { ...state, data: newData };
|
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: {
|
case DELETE_ALL_PENDING_TASKS_SUCCESS: {
|
||||||
const newData = state.data.map((queueInfo) => {
|
const newData = state.data.map((queueInfo) => {
|
||||||
if (queueInfo.name !== action.queue) {
|
if (queueInfo.name !== action.queue) {
|
||||||
@ -446,6 +533,24 @@ function queuesReducer(
|
|||||||
return { ...state, data: newData };
|
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: {
|
case RUN_ALL_SCHEDULED_TASKS_SUCCESS: {
|
||||||
const newData = state.data.map((queueInfo) => {
|
const newData = state.data.map((queueInfo) => {
|
||||||
if (queueInfo.name !== action.queue) {
|
if (queueInfo.name !== action.queue) {
|
||||||
@ -483,6 +588,24 @@ function queuesReducer(
|
|||||||
return { ...state, data: newData };
|
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: {
|
case DELETE_ALL_SCHEDULED_TASKS_SUCCESS: {
|
||||||
const newData = state.data.map((queueInfo) => {
|
const newData = state.data.map((queueInfo) => {
|
||||||
if (queueInfo.name !== action.queue) {
|
if (queueInfo.name !== action.queue) {
|
||||||
@ -550,8 +673,7 @@ function queuesReducer(
|
|||||||
queueInfo.currentStats.pending +
|
queueInfo.currentStats.pending +
|
||||||
action.payload.archived_ids.length,
|
action.payload.archived_ids.length,
|
||||||
retry:
|
retry:
|
||||||
queueInfo.currentStats.retry -
|
queueInfo.currentStats.retry - action.payload.archived_ids.length,
|
||||||
action.payload.archived_ids.length,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -577,6 +699,68 @@ function queuesReducer(
|
|||||||
return { ...state, data: newData };
|
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: {
|
case RUN_ALL_RETRY_TASKS_SUCCESS: {
|
||||||
const newData = state.data.map((queueInfo) => {
|
const newData = state.data.map((queueInfo) => {
|
||||||
if (queueInfo.name !== action.queue) {
|
if (queueInfo.name !== action.queue) {
|
||||||
@ -647,6 +831,23 @@ function queuesReducer(
|
|||||||
return { ...state, data: newData };
|
return { ...state, data: newData };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case DELETE_COMPLETED_TASK_SUCCESS: {
|
||||||
|
const newData = state.data.map((queueInfo) => {
|
||||||
|
if (queueInfo.name !== action.queue) {
|
||||||
|
return queueInfo;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...queueInfo,
|
||||||
|
currentStats: {
|
||||||
|
...queueInfo.currentStats,
|
||||||
|
size: queueInfo.currentStats.size - 1,
|
||||||
|
completed: queueInfo.currentStats.completed - 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return { ...state, data: newData };
|
||||||
|
}
|
||||||
|
|
||||||
case BATCH_RUN_ARCHIVED_TASKS_SUCCESS: {
|
case BATCH_RUN_ARCHIVED_TASKS_SUCCESS: {
|
||||||
const newData = state.data.map((queueInfo) => {
|
const newData = state.data.map((queueInfo) => {
|
||||||
if (queueInfo.name !== action.queue) {
|
if (queueInfo.name !== action.queue) {
|
||||||
@ -688,6 +889,26 @@ function queuesReducer(
|
|||||||
return { ...state, data: newData };
|
return { ...state, data: newData };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case BATCH_DELETE_COMPLETED_TASKS_SUCCESS: {
|
||||||
|
const newData = state.data.map((queueInfo) => {
|
||||||
|
if (queueInfo.name !== action.queue) {
|
||||||
|
return queueInfo;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...queueInfo,
|
||||||
|
currentStats: {
|
||||||
|
...queueInfo.currentStats,
|
||||||
|
size:
|
||||||
|
queueInfo.currentStats.size - action.payload.deleted_ids.length,
|
||||||
|
completed:
|
||||||
|
queueInfo.currentStats.completed -
|
||||||
|
action.payload.deleted_ids.length,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return { ...state, data: newData };
|
||||||
|
}
|
||||||
|
|
||||||
case RUN_ALL_ARCHIVED_TASKS_SUCCESS: {
|
case RUN_ALL_ARCHIVED_TASKS_SUCCESS: {
|
||||||
const newData = state.data.map((queueInfo) => {
|
const newData = state.data.map((queueInfo) => {
|
||||||
if (queueInfo.name !== action.queue) {
|
if (queueInfo.name !== action.queue) {
|
||||||
@ -723,6 +944,36 @@ function queuesReducer(
|
|||||||
return { ...state, data: newData };
|
return { ...state, data: newData };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case DELETE_ALL_COMPLETED_TASKS_SUCCESS: {
|
||||||
|
const newData = state.data.map((queueInfo) => {
|
||||||
|
if (queueInfo.name !== action.queue) {
|
||||||
|
return queueInfo;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...queueInfo,
|
||||||
|
currentStats: {
|
||||||
|
...queueInfo.currentStats,
|
||||||
|
size: queueInfo.currentStats.size - action.deleted,
|
||||||
|
completed: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return { ...state, data: newData };
|
||||||
|
}
|
||||||
|
|
||||||
|
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:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,18 @@ import {
|
|||||||
BATCH_DELETE_PENDING_TASKS_SUCCESS,
|
BATCH_DELETE_PENDING_TASKS_SUCCESS,
|
||||||
ARCHIVE_ALL_PENDING_TASKS_SUCCESS,
|
ARCHIVE_ALL_PENDING_TASKS_SUCCESS,
|
||||||
DELETE_ALL_PENDING_TASKS_SUCCESS,
|
DELETE_ALL_PENDING_TASKS_SUCCESS,
|
||||||
|
DELETE_COMPLETED_TASK_SUCCESS,
|
||||||
|
DELETE_ALL_COMPLETED_TASKS_SUCCESS,
|
||||||
|
BATCH_DELETE_COMPLETED_TASKS_SUCCESS,
|
||||||
|
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";
|
} from "../actions/tasksActions";
|
||||||
|
|
||||||
interface SnackbarState {
|
interface SnackbarState {
|
||||||
@ -95,6 +107,12 @@ function snackbarReducer(
|
|||||||
message: `Archived task is now pending`,
|
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:
|
case ARCHIVE_PENDING_TASK_SUCCESS:
|
||||||
return {
|
return {
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
@ -113,6 +131,12 @@ function snackbarReducer(
|
|||||||
message: `Retry task is now archived`,
|
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:
|
case DELETE_PENDING_TASK_SUCCESS:
|
||||||
return {
|
return {
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
@ -125,6 +149,12 @@ function snackbarReducer(
|
|||||||
message: `Scheduled task deleted`,
|
message: `Scheduled task deleted`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case DELETE_AGGREGATING_TASK_SUCCESS:
|
||||||
|
return {
|
||||||
|
isOpen: true,
|
||||||
|
message: `Aggregating task deleted`,
|
||||||
|
};
|
||||||
|
|
||||||
case BATCH_RUN_SCHEDULED_TASKS_SUCCESS: {
|
case BATCH_RUN_SCHEDULED_TASKS_SUCCESS: {
|
||||||
const n = action.payload.pending_ids.length;
|
const n = action.payload.pending_ids.length;
|
||||||
return {
|
return {
|
||||||
@ -135,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: {
|
case BATCH_ARCHIVE_PENDING_TASKS_SUCCESS: {
|
||||||
const n = action.payload.archived_ids.length;
|
const n = action.payload.archived_ids.length;
|
||||||
return {
|
return {
|
||||||
@ -183,6 +241,24 @@ function snackbarReducer(
|
|||||||
message: "All pending tasks deleted",
|
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:
|
case RUN_ALL_SCHEDULED_TASKS_SUCCESS:
|
||||||
return {
|
return {
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
@ -285,6 +361,25 @@ function snackbarReducer(
|
|||||||
message: "All archived tasks deleted",
|
message: "All archived tasks deleted",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case DELETE_COMPLETED_TASK_SUCCESS:
|
||||||
|
return {
|
||||||
|
isOpen: true,
|
||||||
|
message: `Completed task deleted`,
|
||||||
|
};
|
||||||
|
|
||||||
|
case DELETE_ALL_COMPLETED_TASKS_SUCCESS:
|
||||||
|
return {
|
||||||
|
isOpen: true,
|
||||||
|
message: "All completed tasks deleted",
|
||||||
|
};
|
||||||
|
|
||||||
|
case BATCH_DELETE_COMPLETED_TASKS_SUCCESS:
|
||||||
|
const n = action.payload.deleted_ids.length;
|
||||||
|
return {
|
||||||
|
isOpen: true,
|
||||||
|
message: `${n} completed ${n === 1 ? "task" : "tasks"} deleted`,
|
||||||
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,9 @@ import {
|
|||||||
LIST_ARCHIVED_TASKS_BEGIN,
|
LIST_ARCHIVED_TASKS_BEGIN,
|
||||||
LIST_ARCHIVED_TASKS_SUCCESS,
|
LIST_ARCHIVED_TASKS_SUCCESS,
|
||||||
LIST_ARCHIVED_TASKS_ERROR,
|
LIST_ARCHIVED_TASKS_ERROR,
|
||||||
|
LIST_COMPLETED_TASKS_BEGIN,
|
||||||
|
LIST_COMPLETED_TASKS_SUCCESS,
|
||||||
|
LIST_COMPLETED_TASKS_ERROR,
|
||||||
CANCEL_ACTIVE_TASK_BEGIN,
|
CANCEL_ACTIVE_TASK_BEGIN,
|
||||||
CANCEL_ACTIVE_TASK_SUCCESS,
|
CANCEL_ACTIVE_TASK_SUCCESS,
|
||||||
CANCEL_ACTIVE_TASK_ERROR,
|
CANCEL_ACTIVE_TASK_ERROR,
|
||||||
@ -117,48 +120,58 @@ import {
|
|||||||
GET_TASK_INFO_BEGIN,
|
GET_TASK_INFO_BEGIN,
|
||||||
GET_TASK_INFO_ERROR,
|
GET_TASK_INFO_ERROR,
|
||||||
GET_TASK_INFO_SUCCESS,
|
GET_TASK_INFO_SUCCESS,
|
||||||
|
DELETE_COMPLETED_TASK_BEGIN,
|
||||||
|
DELETE_COMPLETED_TASK_ERROR,
|
||||||
|
DELETE_COMPLETED_TASK_SUCCESS,
|
||||||
|
DELETE_ALL_COMPLETED_TASKS_BEGIN,
|
||||||
|
DELETE_ALL_COMPLETED_TASKS_ERROR,
|
||||||
|
DELETE_ALL_COMPLETED_TASKS_SUCCESS,
|
||||||
|
BATCH_DELETE_COMPLETED_TASKS_BEGIN,
|
||||||
|
BATCH_DELETE_COMPLETED_TASKS_ERROR,
|
||||||
|
BATCH_DELETE_COMPLETED_TASKS_SUCCESS,
|
||||||
|
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";
|
} from "../actions/tasksActions";
|
||||||
import {
|
import { TaskInfo } from "../api";
|
||||||
ActiveTask,
|
|
||||||
ArchivedTask,
|
|
||||||
PendingTask,
|
|
||||||
RetryTask,
|
|
||||||
ScheduledTask,
|
|
||||||
TaskInfo,
|
|
||||||
} from "../api";
|
|
||||||
|
|
||||||
export interface ActiveTaskExtended extends ActiveTask {
|
export interface TaskInfoExtended extends TaskInfo {
|
||||||
// Indicates that a request has been sent for this
|
// Indicates that a request has been sent for this
|
||||||
// task and awaiting for a response.
|
// task and awaiting for a response.
|
||||||
requestPending: boolean;
|
requestPending: boolean;
|
||||||
|
|
||||||
// Incidates that a cancelation signal has been
|
// Incidates that a cancelation signal has been
|
||||||
// published for this task.
|
// published for this task.
|
||||||
canceling: boolean;
|
//
|
||||||
}
|
// Only applies to active tasks
|
||||||
|
canceling?: boolean;
|
||||||
export interface PendingTaskExtended extends PendingTask {
|
|
||||||
// Indicates that a request has been sent for this
|
|
||||||
// task and awaiting for a response.
|
|
||||||
requestPending: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ScheduledTaskExtended extends ScheduledTask {
|
|
||||||
// Indicates that a request has been sent for this
|
|
||||||
// task and awaiting for a response.
|
|
||||||
requestPending: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RetryTaskExtended extends RetryTask {
|
|
||||||
// Indicates that a request has been sent for this
|
|
||||||
// task and awaiting for a response.
|
|
||||||
requestPending: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ArchivedTaskExtended extends ArchivedTask {
|
|
||||||
// Indicates that a request has been sent for this
|
|
||||||
// task and awaiting for a response.
|
|
||||||
requestPending: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TasksState {
|
interface TasksState {
|
||||||
@ -167,41 +180,56 @@ interface TasksState {
|
|||||||
batchActionPending: boolean;
|
batchActionPending: boolean;
|
||||||
allActionPending: boolean;
|
allActionPending: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
data: ActiveTaskExtended[];
|
data: TaskInfoExtended[];
|
||||||
};
|
};
|
||||||
pendingTasks: {
|
pendingTasks: {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
batchActionPending: boolean;
|
batchActionPending: boolean;
|
||||||
allActionPending: boolean;
|
allActionPending: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
data: PendingTaskExtended[];
|
data: TaskInfoExtended[];
|
||||||
};
|
};
|
||||||
scheduledTasks: {
|
scheduledTasks: {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
batchActionPending: boolean;
|
batchActionPending: boolean;
|
||||||
allActionPending: boolean;
|
allActionPending: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
data: ScheduledTaskExtended[];
|
data: TaskInfoExtended[];
|
||||||
};
|
};
|
||||||
retryTasks: {
|
retryTasks: {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
batchActionPending: boolean;
|
batchActionPending: boolean;
|
||||||
allActionPending: boolean;
|
allActionPending: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
data: RetryTaskExtended[];
|
data: TaskInfoExtended[];
|
||||||
};
|
};
|
||||||
archivedTasks: {
|
archivedTasks: {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
batchActionPending: boolean;
|
batchActionPending: boolean;
|
||||||
allActionPending: boolean;
|
allActionPending: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
data: ArchivedTaskExtended[];
|
data: TaskInfoExtended[];
|
||||||
|
};
|
||||||
|
completedTasks: {
|
||||||
|
loading: boolean;
|
||||||
|
batchActionPending: boolean;
|
||||||
|
allActionPending: boolean;
|
||||||
|
error: string;
|
||||||
|
data: TaskInfoExtended[];
|
||||||
|
};
|
||||||
|
aggregatingTasks: {
|
||||||
|
group: string;
|
||||||
|
loading: boolean;
|
||||||
|
batchActionPending: boolean;
|
||||||
|
allActionPending: boolean;
|
||||||
|
error: string;
|
||||||
|
data: TaskInfoExtended[];
|
||||||
};
|
};
|
||||||
taskInfo: {
|
taskInfo: {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
data?: TaskInfo;
|
data?: TaskInfo;
|
||||||
},
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: TasksState = {
|
const initialState: TasksState = {
|
||||||
@ -240,10 +268,25 @@ const initialState: TasksState = {
|
|||||||
error: "",
|
error: "",
|
||||||
data: [],
|
data: [],
|
||||||
},
|
},
|
||||||
|
completedTasks: {
|
||||||
|
loading: false,
|
||||||
|
batchActionPending: false,
|
||||||
|
allActionPending: false,
|
||||||
|
error: "",
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
aggregatingTasks: {
|
||||||
|
group: "",
|
||||||
|
loading: false,
|
||||||
|
batchActionPending: false,
|
||||||
|
allActionPending: false,
|
||||||
|
error: "",
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
taskInfo: {
|
taskInfo: {
|
||||||
loading: false,
|
loading: false,
|
||||||
error: "",
|
error: "",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function tasksReducer(
|
function tasksReducer(
|
||||||
@ -258,16 +301,16 @@ function tasksReducer(
|
|||||||
...state.taskInfo,
|
...state.taskInfo,
|
||||||
loading: true,
|
loading: true,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
case GET_TASK_INFO_ERROR:
|
case GET_TASK_INFO_ERROR:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
taskInfo: {
|
taskInfo: {
|
||||||
loading: false,
|
loading: false,
|
||||||
error: action.error,
|
error: action.error,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
case GET_TASK_INFO_SUCCESS:
|
case GET_TASK_INFO_SUCCESS:
|
||||||
return {
|
return {
|
||||||
@ -450,6 +493,194 @@ function tasksReducer(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case LIST_COMPLETED_TASKS_BEGIN:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
completedTasks: {
|
||||||
|
...state.completedTasks,
|
||||||
|
loading: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
case LIST_COMPLETED_TASKS_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
completedTasks: {
|
||||||
|
...state.completedTasks,
|
||||||
|
loading: false,
|
||||||
|
error: "",
|
||||||
|
data: action.payload.tasks.map((task) => ({
|
||||||
|
...task,
|
||||||
|
requestPending: false,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
case LIST_COMPLETED_TASKS_ERROR:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
completedTasks: {
|
||||||
|
...state.completedTasks,
|
||||||
|
loading: false,
|
||||||
|
error: action.error,
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
case 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,
|
||||||
|
completedTasks: {
|
||||||
|
...state.completedTasks,
|
||||||
|
data: state.completedTasks.data.map((task) => {
|
||||||
|
if (task.id !== action.taskId) {
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
return { ...task, requestPending: true };
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
case DELETE_COMPLETED_TASK_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
completedTasks: {
|
||||||
|
...state.completedTasks,
|
||||||
|
data: state.completedTasks.data.filter(
|
||||||
|
(task) => task.id !== action.taskId
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
case DELETE_COMPLETED_TASK_ERROR:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
completedTasks: {
|
||||||
|
...state.completedTasks,
|
||||||
|
data: state.completedTasks.data.map((task) => {
|
||||||
|
if (task.id !== action.taskId) {
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
return { ...task, requestPending: false };
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
case DELETE_ALL_COMPLETED_TASKS_BEGIN:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
completedTasks: {
|
||||||
|
...state.completedTasks,
|
||||||
|
allActionPending: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
case DELETE_ALL_COMPLETED_TASKS_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
completedTasks: {
|
||||||
|
...state.completedTasks,
|
||||||
|
allActionPending: false,
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
case DELETE_ALL_COMPLETED_TASKS_ERROR:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
completedTasks: {
|
||||||
|
...state.completedTasks,
|
||||||
|
allActionPending: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
case BATCH_DELETE_COMPLETED_TASKS_BEGIN:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
completedTasks: {
|
||||||
|
...state.completedTasks,
|
||||||
|
batchActionPending: true,
|
||||||
|
data: state.completedTasks.data.map((task) => {
|
||||||
|
if (!action.taskIds.includes(task.id)) {
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...task,
|
||||||
|
requestPending: true,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
case BATCH_DELETE_COMPLETED_TASKS_SUCCESS: {
|
||||||
|
const newData = state.completedTasks.data.filter(
|
||||||
|
(task) => !action.payload.deleted_ids.includes(task.id)
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
completedTasks: {
|
||||||
|
...state.completedTasks,
|
||||||
|
batchActionPending: false,
|
||||||
|
data: newData,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case BATCH_DELETE_COMPLETED_TASKS_ERROR:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
completedTasks: {
|
||||||
|
...state.completedTasks,
|
||||||
|
batchActionPending: false,
|
||||||
|
data: state.completedTasks.data.map((task) => {
|
||||||
|
if (!action.taskIds.includes(task.id)) {
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...task,
|
||||||
|
requestPending: false,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
case CANCEL_ACTIVE_TASK_BEGIN: {
|
case CANCEL_ACTIVE_TASK_BEGIN: {
|
||||||
const newData = state.activeTasks.data.map((task) => {
|
const newData = state.activeTasks.data.map((task) => {
|
||||||
if (task.id !== action.taskId) {
|
if (task.id !== action.taskId) {
|
||||||
@ -894,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 RUN_RETRY_TASK_BEGIN:
|
||||||
case ARCHIVE_RETRY_TASK_BEGIN:
|
case ARCHIVE_RETRY_TASK_BEGIN:
|
||||||
case DELETE_RETRY_TASK_BEGIN:
|
case DELETE_RETRY_TASK_BEGIN:
|
||||||
@ -1194,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:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -2,22 +2,26 @@ import { combineReducers, configureStore } from "@reduxjs/toolkit";
|
|||||||
import settingsReducer from "./reducers/settingsReducer";
|
import settingsReducer from "./reducers/settingsReducer";
|
||||||
import queuesReducer from "./reducers/queuesReducer";
|
import queuesReducer from "./reducers/queuesReducer";
|
||||||
import tasksReducer from "./reducers/tasksReducer";
|
import tasksReducer from "./reducers/tasksReducer";
|
||||||
|
import groupsReducer from "./reducers/groupsReducer";
|
||||||
import serversReducer from "./reducers/serversReducer";
|
import serversReducer from "./reducers/serversReducer";
|
||||||
import schedulerEntriesReducer from "./reducers/schedulerEntriesReducer";
|
import schedulerEntriesReducer from "./reducers/schedulerEntriesReducer";
|
||||||
import snackbarReducer from "./reducers/snackbarReducer";
|
import snackbarReducer from "./reducers/snackbarReducer";
|
||||||
import queueStatsReducer from "./reducers/queueStatsReducer";
|
import queueStatsReducer from "./reducers/queueStatsReducer";
|
||||||
import redisInfoReducer from "./reducers/redisInfoReducer";
|
import redisInfoReducer from "./reducers/redisInfoReducer";
|
||||||
|
import metricsReducer from "./reducers/metricsReducer";
|
||||||
import { loadState } from "./localStorage";
|
import { loadState } from "./localStorage";
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
settings: settingsReducer,
|
settings: settingsReducer,
|
||||||
queues: queuesReducer,
|
queues: queuesReducer,
|
||||||
tasks: tasksReducer,
|
tasks: tasksReducer,
|
||||||
|
groups: groupsReducer,
|
||||||
servers: serversReducer,
|
servers: serversReducer,
|
||||||
schedulerEntries: schedulerEntriesReducer,
|
schedulerEntries: schedulerEntriesReducer,
|
||||||
snackbar: snackbarReducer,
|
snackbar: snackbarReducer,
|
||||||
queueStats: queueStatsReducer,
|
queueStats: queueStatsReducer,
|
||||||
redis: redisInfoReducer,
|
redis: redisInfoReducer,
|
||||||
|
metrics: metricsReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
const preloadedState = loadState();
|
const preloadedState = loadState();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createMuiTheme, Theme } from "@material-ui/core/styles";
|
import { createTheme, Theme } from "@material-ui/core/styles";
|
||||||
import { ThemePreference } from "./reducers/settingsReducer";
|
import { ThemePreference } from "./reducers/settingsReducer";
|
||||||
import useMediaQuery from "@material-ui/core/useMediaQuery";
|
import useMediaQuery from "@material-ui/core/useMediaQuery";
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ export function useTheme(themePreference: ThemePreference): Theme {
|
|||||||
} else if (themePreference === ThemePreference.Never) {
|
} else if (themePreference === ThemePreference.Never) {
|
||||||
prefersDarkMode = false;
|
prefersDarkMode = false;
|
||||||
}
|
}
|
||||||
return createMuiTheme({
|
return createTheme({
|
||||||
// Got color palette from https://htmlcolors.com/palette/31/stripe
|
// Got color palette from https://htmlcolors.com/palette/31/stripe
|
||||||
palette: {
|
palette: {
|
||||||
primary: {
|
primary: {
|
||||||
|
8
ui/src/types/taskState.ts
Normal file
8
ui/src/types/taskState.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export type TaskState =
|
||||||
|
| "active"
|
||||||
|
| "pending"
|
||||||
|
| "aggregating"
|
||||||
|
| "scheduled"
|
||||||
|
| "retry"
|
||||||
|
| "archived"
|
||||||
|
| "completed";
|
@ -25,17 +25,22 @@ interface Duration {
|
|||||||
totalSeconds: number;
|
totalSeconds: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// start and end are in milliseconds.
|
// Returns a duration from the number of seconds provided.
|
||||||
function durationBetween(start: number, end: number): Duration {
|
export function durationFromSeconds(totalSeconds: number): Duration {
|
||||||
const durationInMillisec = start - end;
|
|
||||||
const totalSeconds = Math.floor(durationInMillisec / 1000);
|
|
||||||
const hour = Math.floor(totalSeconds / 3600);
|
const hour = Math.floor(totalSeconds / 3600);
|
||||||
const minute = Math.floor((totalSeconds - 3600 * hour) / 60);
|
const minute = Math.floor((totalSeconds - 3600 * hour) / 60);
|
||||||
const second = totalSeconds - 3600 * hour - 60 * minute;
|
const second = totalSeconds - 3600 * hour - 60 * minute;
|
||||||
return { hour, minute, second, totalSeconds };
|
return { hour, minute, second, totalSeconds };
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringifyDuration(d: Duration): string {
|
// start and end are in milliseconds.
|
||||||
|
function durationBetween(start: number, end: number): Duration {
|
||||||
|
const durationInMillisec = start - end;
|
||||||
|
const totalSeconds = Math.floor(durationInMillisec / 1000);
|
||||||
|
return durationFromSeconds(totalSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stringifyDuration(d: Duration): string {
|
||||||
if (d.hour > 24) {
|
if (d.hour > 24) {
|
||||||
const n = Math.floor(d.hour / 24);
|
const n = Math.floor(d.hour / 24);
|
||||||
return n + (n === 1 ? " day" : " days");
|
return n + (n === 1 ? " day" : " days");
|
||||||
@ -59,7 +64,11 @@ export function durationBefore(timestamp: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const zeroTimestamp = "0001-01-01T00:00:00Z";
|
||||||
export function timeAgo(timestamp: string): string {
|
export function timeAgo(timestamp: string): string {
|
||||||
|
if (timestamp === zeroTimestamp) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return timeAgoUnix(Date.parse(timestamp) / 1000);
|
return timeAgoUnix(Date.parse(timestamp) / 1000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -69,8 +78,8 @@ export function timeAgo(timestamp: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function timeAgoUnix(unixtime: number): string {
|
export function timeAgoUnix(unixtime: number): string {
|
||||||
if (unixtime === 0) {
|
if (unixtime === 0) {
|
||||||
return ""
|
return "";
|
||||||
}
|
}
|
||||||
const duration = durationBetween(Date.now(), unixtime * 1000);
|
const duration = durationBetween(Date.now(), unixtime * 1000);
|
||||||
return stringifyDuration(duration) + " ago";
|
return stringifyDuration(duration) + " ago";
|
||||||
@ -97,3 +106,45 @@ export function percentage(numerator: number, denominator: number): string {
|
|||||||
const perc = ((numerator / denominator) * 100).toFixed(2);
|
const perc = ((numerator / denominator) * 100).toFixed(2);
|
||||||
return `${perc} %`;
|
return `${perc} %`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isJsonPayload(p: string) {
|
||||||
|
try {
|
||||||
|
JSON.parse(p);
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prettifyPayload(p: string) {
|
||||||
|
if (isJsonPayload(p)) {
|
||||||
|
return JSON.stringify(JSON.parse(p), null, 2);
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC.
|
||||||
|
export function currentUnixtime(): number {
|
||||||
|
return Math.floor(Date.now() / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const durationRegex = /([0-9]*(\.[0-9]*)?)[s|m|h]/;
|
||||||
|
// Parses the given string and returns the number of seconds if the input is valid.
|
||||||
|
// Otherwise, it throws an error
|
||||||
|
// Supported time units are "s", "m", "h"
|
||||||
|
export function parseDuration(s: string): number {
|
||||||
|
if (!durationRegex.test(s)) {
|
||||||
|
throw new Error("invalid duration");
|
||||||
|
}
|
||||||
|
const val = parseFloat(s.slice(0, -1));
|
||||||
|
switch (s.slice(-1)) {
|
||||||
|
case "s":
|
||||||
|
return val;
|
||||||
|
case "m":
|
||||||
|
return val * 60;
|
||||||
|
case "h":
|
||||||
|
return val * 60 * 60;
|
||||||
|
default:
|
||||||
|
throw new Error("invalid duration unit");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
329
ui/src/views/MetricsView.tsx
Normal file
329
ui/src/views/MetricsView.tsx
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { connect, ConnectedProps } from "react-redux";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
import queryString from "query-string";
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
import Container from "@material-ui/core/Container";
|
||||||
|
import Grid from "@material-ui/core/Grid";
|
||||||
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
import WarningIcon from "@material-ui/icons/Warning";
|
||||||
|
import InfoIcon from "@material-ui/icons/Info";
|
||||||
|
import prettyBytes from "pretty-bytes";
|
||||||
|
import { getMetricsAsync } from "../actions/metricsActions";
|
||||||
|
import { listQueuesAsync } from "../actions/queuesActions";
|
||||||
|
import { AppState } from "../store";
|
||||||
|
import QueueMetricsChart from "../components/QueueMetricsChart";
|
||||||
|
import Tooltip from "../components/Tooltip";
|
||||||
|
import { currentUnixtime } from "../utils";
|
||||||
|
import MetricsFetchControls from "../components/MetricsFetchControls";
|
||||||
|
import { useQuery } from "../hooks";
|
||||||
|
import { PrometheusMetricsResponse } from "../api";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
container: {
|
||||||
|
marginTop: 30,
|
||||||
|
paddingTop: theme.spacing(4),
|
||||||
|
paddingBottom: theme.spacing(4),
|
||||||
|
},
|
||||||
|
controlsContainer: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
position: "fixed",
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
zIndex: theme.zIndex.appBar,
|
||||||
|
right: 0,
|
||||||
|
top: 64, // app-bar height
|
||||||
|
width: "100%",
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
},
|
||||||
|
chartInfo: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
},
|
||||||
|
infoIcon: {
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
|
errorMessage: {
|
||||||
|
marginLeft: "auto",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
warningIcon: {
|
||||||
|
color: "#ff6700",
|
||||||
|
marginRight: 6,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
function mapStateToProps(state: AppState) {
|
||||||
|
return {
|
||||||
|
loading: state.metrics.loading,
|
||||||
|
error: state.metrics.error,
|
||||||
|
data: state.metrics.data,
|
||||||
|
pollInterval: state.settings.pollInterval,
|
||||||
|
queues: state.queues.data.map((q) => q.name),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const connector = connect(mapStateToProps, {
|
||||||
|
getMetricsAsync,
|
||||||
|
listQueuesAsync,
|
||||||
|
});
|
||||||
|
type Props = ConnectedProps<typeof connector>;
|
||||||
|
|
||||||
|
const ENDTIME_URL_PARAM_KEY = "end";
|
||||||
|
const DURATION_URL_PARAM_KEY = "duration";
|
||||||
|
|
||||||
|
function MetricsView(props: Props) {
|
||||||
|
const classes = useStyles();
|
||||||
|
const history = useHistory();
|
||||||
|
const query = useQuery();
|
||||||
|
|
||||||
|
const endTimeStr = query.get(ENDTIME_URL_PARAM_KEY);
|
||||||
|
const endTime = endTimeStr ? parseFloat(endTimeStr) : currentUnixtime(); // default to now
|
||||||
|
|
||||||
|
const durationStr = query.get(DURATION_URL_PARAM_KEY);
|
||||||
|
const duration = durationStr ? parseFloat(durationStr) : 60 * 60; // default to 1h
|
||||||
|
|
||||||
|
const { pollInterval, getMetricsAsync, listQueuesAsync, data } = props;
|
||||||
|
|
||||||
|
const [endTimeSec, setEndTimeSec] = React.useState(endTime);
|
||||||
|
const [durationSec, setDurationSec] = React.useState(duration);
|
||||||
|
const [selectedQueues, setSelectedQueues] = React.useState<string[]>([]);
|
||||||
|
|
||||||
|
const handleEndTimeChange = (endTime: number, isEndTimeFixed: boolean) => {
|
||||||
|
const urlQuery = isEndTimeFixed
|
||||||
|
? {
|
||||||
|
[ENDTIME_URL_PARAM_KEY]: endTime,
|
||||||
|
[DURATION_URL_PARAM_KEY]: durationSec,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
[DURATION_URL_PARAM_KEY]: durationSec,
|
||||||
|
};
|
||||||
|
history.push({
|
||||||
|
...history.location,
|
||||||
|
search: queryString.stringify(urlQuery),
|
||||||
|
});
|
||||||
|
setEndTimeSec(endTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDurationChange = (duration: number, isEndTimeFixed: boolean) => {
|
||||||
|
const urlQuery = isEndTimeFixed
|
||||||
|
? {
|
||||||
|
[ENDTIME_URL_PARAM_KEY]: endTimeSec,
|
||||||
|
[DURATION_URL_PARAM_KEY]: duration,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
[DURATION_URL_PARAM_KEY]: duration,
|
||||||
|
};
|
||||||
|
history.push({
|
||||||
|
...history.location,
|
||||||
|
search: queryString.stringify(urlQuery),
|
||||||
|
});
|
||||||
|
setDurationSec(duration);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddQueue = (qname: string) => {
|
||||||
|
if (selectedQueues.includes(qname)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedQueues(selectedQueues.concat(qname));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveQueue = (qname: string) => {
|
||||||
|
if (selectedQueues.length === 1) {
|
||||||
|
return; // ensure that selected queues doesn't go down to zero once user selected
|
||||||
|
}
|
||||||
|
if (selectedQueues.length === 0) {
|
||||||
|
// when user first select filter (remove once of the queues),
|
||||||
|
// we need to lazily initialize the selectedQueues with the rest (all queues but the selected one).
|
||||||
|
setSelectedQueues(props.queues.filter((q) => q !== qname));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedQueues(selectedQueues.filter((q) => q !== qname));
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
listQueuesAsync();
|
||||||
|
}, [listQueuesAsync]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
getMetricsAsync(endTimeSec, durationSec, selectedQueues);
|
||||||
|
}, [pollInterval, getMetricsAsync, durationSec, endTimeSec, selectedQueues]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg" className={classes.container}>
|
||||||
|
<div className={classes.controlsContainer}>
|
||||||
|
<MetricsFetchControls
|
||||||
|
endTimeSec={endTimeSec}
|
||||||
|
onEndTimeChange={handleEndTimeChange}
|
||||||
|
durationSec={durationSec}
|
||||||
|
onDurationChange={handleDurationChange}
|
||||||
|
queues={props.queues}
|
||||||
|
selectedQueues={
|
||||||
|
// If none are selected (e.g. initial state), no filters should apply.
|
||||||
|
selectedQueues.length === 0 ? props.queues : selectedQueues
|
||||||
|
}
|
||||||
|
addQueue={handleAddQueue}
|
||||||
|
removeQueue={handleRemoveQueue}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
{data?.tasks_processed_per_second && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<ChartRow
|
||||||
|
title="Tasks Processed"
|
||||||
|
description="Number of tasks processed (both succeeded and failed) per second."
|
||||||
|
metrics={data.tasks_processed_per_second}
|
||||||
|
endTime={endTimeSec}
|
||||||
|
startTime={endTimeSec - durationSec}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{data?.tasks_failed_per_second && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<ChartRow
|
||||||
|
title="Tasks Failed"
|
||||||
|
description="Number of tasks failed per second."
|
||||||
|
metrics={data.tasks_failed_per_second}
|
||||||
|
endTime={endTimeSec}
|
||||||
|
startTime={endTimeSec - durationSec}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{data?.error_rate && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<ChartRow
|
||||||
|
title="Error Rate"
|
||||||
|
description="Rate of task failures"
|
||||||
|
metrics={data.error_rate}
|
||||||
|
endTime={endTimeSec}
|
||||||
|
startTime={endTimeSec - durationSec}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{data?.queue_size && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<ChartRow
|
||||||
|
title="Queue Size"
|
||||||
|
description="Total number of tasks in a given queue."
|
||||||
|
metrics={data.queue_size}
|
||||||
|
endTime={endTimeSec}
|
||||||
|
startTime={endTimeSec - durationSec}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{data?.queue_latency_seconds && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<ChartRow
|
||||||
|
title="Queue Latency"
|
||||||
|
description="Latency of queue, measured by the oldest pending task in the queue."
|
||||||
|
metrics={data.queue_latency_seconds}
|
||||||
|
endTime={endTimeSec}
|
||||||
|
startTime={endTimeSec - durationSec}
|
||||||
|
yAxisTickFormatter={(val: number) => val + "s"}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{data?.queue_size && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<ChartRow
|
||||||
|
title="Queue Memory Usage (approx)"
|
||||||
|
description="Memory usage by queue. Approximate value by sampling a few tasks in a queue."
|
||||||
|
metrics={data.queue_memory_usage_approx_bytes}
|
||||||
|
endTime={endTimeSec}
|
||||||
|
startTime={endTimeSec - durationSec}
|
||||||
|
yAxisTickFormatter={(val: number) => {
|
||||||
|
try {
|
||||||
|
return prettyBytes(val);
|
||||||
|
} catch (error) {
|
||||||
|
return val + "B";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{data?.pending_tasks_by_queue && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<ChartRow
|
||||||
|
title="Pending Tasks"
|
||||||
|
description="Number of pending tasks in a given queue."
|
||||||
|
metrics={data.pending_tasks_by_queue}
|
||||||
|
endTime={endTimeSec}
|
||||||
|
startTime={endTimeSec - durationSec}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{data?.retry_tasks_by_queue && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<ChartRow
|
||||||
|
title="Retry Tasks"
|
||||||
|
description="Number of retry tasks in a given queue."
|
||||||
|
metrics={data.retry_tasks_by_queue}
|
||||||
|
endTime={endTimeSec}
|
||||||
|
startTime={endTimeSec - durationSec}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{data?.archived_tasks_by_queue && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<ChartRow
|
||||||
|
title="Archived Tasks"
|
||||||
|
description="Number of archived tasks in a given queue."
|
||||||
|
metrics={data.archived_tasks_by_queue}
|
||||||
|
endTime={endTimeSec}
|
||||||
|
startTime={endTimeSec - durationSec}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connector(MetricsView);
|
||||||
|
|
||||||
|
/******** Helper components ********/
|
||||||
|
|
||||||
|
interface ChartRowProps {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
metrics: PrometheusMetricsResponse;
|
||||||
|
endTime: number;
|
||||||
|
startTime: number;
|
||||||
|
yAxisTickFormatter?: (val: number) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChartRow(props: ChartRowProps) {
|
||||||
|
const classes = useStyles();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={classes.chartInfo}>
|
||||||
|
<Typography color="textPrimary">{props.title}</Typography>
|
||||||
|
<Tooltip title={<div>{props.description}</div>}>
|
||||||
|
<InfoIcon fontSize="small" className={classes.infoIcon} />
|
||||||
|
</Tooltip>
|
||||||
|
{props.metrics.status === "error" && (
|
||||||
|
<div className={classes.errorMessage}>
|
||||||
|
<WarningIcon fontSize="small" className={classes.warningIcon} />
|
||||||
|
<Typography color="textSecondary">
|
||||||
|
Failed to get metrics data: {props.metrics.error}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<QueueMetricsChart
|
||||||
|
data={
|
||||||
|
props.metrics.status === "error"
|
||||||
|
? []
|
||||||
|
: props.metrics.data?.result || []
|
||||||
|
}
|
||||||
|
endTime={props.endTime}
|
||||||
|
startTime={props.startTime}
|
||||||
|
yAxisTickFormatter={props.yAxisTickFormatter}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -18,6 +18,7 @@ import { TaskDetailsRouteParams } from "../paths";
|
|||||||
import { usePolling } from "../hooks";
|
import { usePolling } from "../hooks";
|
||||||
import { listQueuesAsync } from "../actions/queuesActions";
|
import { listQueuesAsync } from "../actions/queuesActions";
|
||||||
import SyntaxHighlighter from "../components/SyntaxHighlighter";
|
import SyntaxHighlighter from "../components/SyntaxHighlighter";
|
||||||
|
import { durationFromSeconds, stringifyDuration, timeAgo, prettifyPayload } from "../utils";
|
||||||
|
|
||||||
function mapStateToProps(state: AppState) {
|
function mapStateToProps(state: AppState) {
|
||||||
return {
|
return {
|
||||||
@ -175,7 +176,7 @@ function TaskDetailsView(props: Props) {
|
|||||||
{taskInfo?.error_message} ({taskInfo?.last_failed_at})
|
{taskInfo?.error_message} ({taskInfo?.last_failed_at})
|
||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
<Typography>n/a</Typography>
|
<Typography> - </Typography>
|
||||||
)}
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
@ -189,7 +190,7 @@ function TaskDetailsView(props: Props) {
|
|||||||
{taskInfo?.next_process_at ? (
|
{taskInfo?.next_process_at ? (
|
||||||
<Typography>{taskInfo?.next_process_at}</Typography>
|
<Typography>{taskInfo?.next_process_at}</Typography>
|
||||||
) : (
|
) : (
|
||||||
<Typography>n/a</Typography>
|
<Typography> - </Typography>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -201,7 +202,7 @@ function TaskDetailsView(props: Props) {
|
|||||||
{taskInfo?.timeout_seconds ? (
|
{taskInfo?.timeout_seconds ? (
|
||||||
<Typography>{taskInfo?.timeout_seconds} seconds</Typography>
|
<Typography>{taskInfo?.timeout_seconds} seconds</Typography>
|
||||||
) : (
|
) : (
|
||||||
<Typography>n/a</Typography>
|
<Typography> - </Typography>
|
||||||
)}
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
@ -213,7 +214,7 @@ function TaskDetailsView(props: Props) {
|
|||||||
{taskInfo?.deadline ? (
|
{taskInfo?.deadline ? (
|
||||||
<Typography>{taskInfo?.deadline}</Typography>
|
<Typography>{taskInfo?.deadline}</Typography>
|
||||||
) : (
|
) : (
|
||||||
<Typography>n/a</Typography>
|
<Typography> - </Typography>
|
||||||
)}
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
@ -227,11 +228,64 @@ function TaskDetailsView(props: Props) {
|
|||||||
language="json"
|
language="json"
|
||||||
customStyle={{ margin: 0, maxWidth: 400 }}
|
customStyle={{ margin: 0, maxWidth: 400 }}
|
||||||
>
|
>
|
||||||
{taskInfo.payload}
|
{prettifyPayload(taskInfo.payload)}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{
|
||||||
|
/* Completed Task Only */ taskInfo?.state === "completed" && (
|
||||||
|
<>
|
||||||
|
<div className={classes.infoRow}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
className={classes.infoKeyCell}
|
||||||
|
>
|
||||||
|
Completed:{" "}
|
||||||
|
</Typography>
|
||||||
|
<div className={classes.infoValueCell}>
|
||||||
|
<Typography>
|
||||||
|
{timeAgo(taskInfo.completed_at)} (
|
||||||
|
{taskInfo.completed_at})
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={classes.infoRow}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
className={classes.infoKeyCell}
|
||||||
|
>
|
||||||
|
Result:{" "}
|
||||||
|
</Typography>
|
||||||
|
<div className={classes.infoValueCell}>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
language="json"
|
||||||
|
customStyle={{ margin: 0, maxWidth: 400 }}
|
||||||
|
>
|
||||||
|
{prettifyPayload(taskInfo.result)}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={classes.infoRow}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
className={classes.infoKeyCell}
|
||||||
|
>
|
||||||
|
TTL:{" "}
|
||||||
|
</Typography>
|
||||||
|
<Typography className={classes.infoValueCell}>
|
||||||
|
<Typography>
|
||||||
|
{taskInfo.ttl_seconds > 0
|
||||||
|
? `${stringifyDuration(
|
||||||
|
durationFromSeconds(taskInfo.ttl_seconds)
|
||||||
|
)} left`
|
||||||
|
: "expired"}
|
||||||
|
</Typography>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
</Paper>
|
</Paper>
|
||||||
)}
|
)}
|
||||||
<div className={classes.footer}>
|
<div className={classes.footer}>
|
||||||
|
@ -3,13 +3,14 @@ import { connect, ConnectedProps } from "react-redux";
|
|||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import Container from "@material-ui/core/Container";
|
import Container from "@material-ui/core/Container";
|
||||||
import Grid from "@material-ui/core/Grid";
|
import Grid from "@material-ui/core/Grid";
|
||||||
import TasksTable from "../components/TasksTable";
|
import TasksTableContainer from "../components/TasksTableContainer";
|
||||||
import QueueInfoBanner from "../components/QueueInfoBanner";
|
import QueueInfoBanner from "../components/QueueInfoBanner";
|
||||||
import QueueBreadCrumb from "../components/QueueBreadcrumb";
|
import QueueBreadCrumb from "../components/QueueBreadcrumb";
|
||||||
import { useParams, useLocation } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { listQueuesAsync } from "../actions/queuesActions";
|
import { listQueuesAsync } from "../actions/queuesActions";
|
||||||
import { AppState } from "../store";
|
import { AppState } from "../store";
|
||||||
import { QueueDetailsRouteParams } from "../paths";
|
import { QueueDetailsRouteParams } from "../paths";
|
||||||
|
import { useQuery } from "../hooks";
|
||||||
|
|
||||||
function mapStateToProps(state: AppState) {
|
function mapStateToProps(state: AppState) {
|
||||||
return {
|
return {
|
||||||
@ -34,11 +35,15 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function useQuery(): URLSearchParams {
|
const validStatus = [
|
||||||
return new URLSearchParams(useLocation().search);
|
"active",
|
||||||
}
|
"pending",
|
||||||
|
"aggregating",
|
||||||
const validStatus = ["active", "pending", "scheduled", "retry", "archived"];
|
"scheduled",
|
||||||
|
"retry",
|
||||||
|
"archived",
|
||||||
|
"completed",
|
||||||
|
];
|
||||||
const defaultStatus = "active";
|
const defaultStatus = "active";
|
||||||
|
|
||||||
function TasksView(props: ConnectedProps<typeof connector>) {
|
function TasksView(props: ConnectedProps<typeof connector>) {
|
||||||
@ -65,7 +70,7 @@ function TasksView(props: ConnectedProps<typeof connector>) {
|
|||||||
<QueueInfoBanner qname={qname} />
|
<QueueInfoBanner qname={qname} />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} className={classes.tasksTable}>
|
<Grid item xs={12} className={classes.tasksTable}>
|
||||||
<TasksTable queue={qname} selected={selected} />
|
<TasksTableContainer queue={qname} selected={selected} />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Container>
|
</Container>
|
||||||
|
348
ui/yarn.lock
348
ui/yarn.lock
@ -1111,10 +1111,17 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
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.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":
|
"@babel/runtime@^7.0.0":
|
||||||
version "7.13.9"
|
version "7.17.8"
|
||||||
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.9.tgz#97dbe2116e2630c489f22e0656decd60aaa1fcee"
|
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2"
|
||||||
integrity sha512-aY2kU+xgJ3dJ1eU6FMB9EH8dIe8dmusF1xEku52joLvw6eAFN0AI+WxCLDnpev2LEejWBAy2sBvBOBAjI3zmvA==
|
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"
|
||||||
|
integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
@ -1413,15 +1420,26 @@
|
|||||||
"@types/yargs" "^15.0.0"
|
"@types/yargs" "^15.0.0"
|
||||||
chalk "^4.0.0"
|
chalk "^4.0.0"
|
||||||
|
|
||||||
"@material-ui/core@4.11.3":
|
"@jest/types@^27.2.5":
|
||||||
version "4.11.3"
|
version "27.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.11.3.tgz#f22e41775b0bd075e36a7a093d43951bf7f63850"
|
resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.2.5.tgz#420765c052605e75686982d24b061b4cbba22132"
|
||||||
integrity sha512-Adt40rGW6Uds+cAyk3pVgcErpzU/qxc7KBR94jFHBYretU4AtWZltYcNsbeMn9tXL86jjVL1kuGcIHsgLgFGRw==
|
integrity sha512-nmuM4VuDtCZcY+eTpw+0nvstwReMsjPoj7ZR80/BbixulhLaiX+fbv8oeLW8WZlJMcsGQsTmMKT/iTZu1Uy/lQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/istanbul-lib-coverage" "^2.0.0"
|
||||||
|
"@types/istanbul-reports" "^3.0.0"
|
||||||
|
"@types/node" "*"
|
||||||
|
"@types/yargs" "^16.0.0"
|
||||||
|
chalk "^4.0.0"
|
||||||
|
|
||||||
|
"@material-ui/core@4.12.3":
|
||||||
|
version "4.12.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.12.3.tgz#80d665caf0f1f034e52355c5450c0e38b099d3ca"
|
||||||
|
integrity sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.4.4"
|
"@babel/runtime" "^7.4.4"
|
||||||
"@material-ui/styles" "^4.11.3"
|
"@material-ui/styles" "^4.11.4"
|
||||||
"@material-ui/system" "^4.11.3"
|
"@material-ui/system" "^4.12.1"
|
||||||
"@material-ui/types" "^5.1.0"
|
"@material-ui/types" "5.1.0"
|
||||||
"@material-ui/utils" "^4.11.2"
|
"@material-ui/utils" "^4.11.2"
|
||||||
"@types/react-transition-group" "^4.2.0"
|
"@types/react-transition-group" "^4.2.0"
|
||||||
clsx "^1.0.4"
|
clsx "^1.0.4"
|
||||||
@ -1438,10 +1456,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.4.4"
|
"@babel/runtime" "^7.4.4"
|
||||||
|
|
||||||
"@material-ui/lab@4.0.0-alpha.57":
|
"@material-ui/lab@4.0.0-alpha.58":
|
||||||
version "4.0.0-alpha.57"
|
version "4.0.0-alpha.58"
|
||||||
resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.57.tgz#e8961bcf6449e8a8dabe84f2700daacfcafbf83a"
|
resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.58.tgz#c7ebb66f49863c5acbb20817163737caa299fafc"
|
||||||
integrity sha512-qo/IuIQOmEKtzmRD2E4Aa6DB4A87kmY6h0uYhjUmrrgmEAgbbw9etXpWPVXuRK6AGIQCjFzV6WO2i21m1R4FCw==
|
integrity sha512-GKHlJqLxUeHH3L3dGQ48ZavYrqGOTXkFkiEiuYMAnAvXAZP4rhMIqeHOPXSUQan4Bd8QnafDcpovOSLnadDmKw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.4.4"
|
"@babel/runtime" "^7.4.4"
|
||||||
"@material-ui/utils" "^4.11.2"
|
"@material-ui/utils" "^4.11.2"
|
||||||
@ -1449,14 +1467,14 @@
|
|||||||
prop-types "^15.7.2"
|
prop-types "^15.7.2"
|
||||||
react-is "^16.8.0 || ^17.0.0"
|
react-is "^16.8.0 || ^17.0.0"
|
||||||
|
|
||||||
"@material-ui/styles@^4.11.3":
|
"@material-ui/styles@^4.11.4":
|
||||||
version "4.11.3"
|
version "4.11.4"
|
||||||
resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.3.tgz#1b8d97775a4a643b53478c895e3f2a464e8916f2"
|
resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.4.tgz#eb9dfccfcc2d208243d986457dff025497afa00d"
|
||||||
integrity sha512-HzVzCG+PpgUGMUYEJ2rTEmQYeonGh41BYfILNFb/1ueqma+p1meSdu4RX6NjxYBMhf7k+jgfHFTTz+L1SXL/Zg==
|
integrity sha512-KNTIZcnj/zprG5LW0Sao7zw+yG3O35pviHzejMdcSGCdWbiO8qzRgOYL8JAxAsWBKOKYwVZxXtHWaB5T2Kvxew==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.4.4"
|
"@babel/runtime" "^7.4.4"
|
||||||
"@emotion/hash" "^0.8.0"
|
"@emotion/hash" "^0.8.0"
|
||||||
"@material-ui/types" "^5.1.0"
|
"@material-ui/types" "5.1.0"
|
||||||
"@material-ui/utils" "^4.11.2"
|
"@material-ui/utils" "^4.11.2"
|
||||||
clsx "^1.0.4"
|
clsx "^1.0.4"
|
||||||
csstype "^2.5.2"
|
csstype "^2.5.2"
|
||||||
@ -1471,19 +1489,19 @@
|
|||||||
jss-plugin-vendor-prefixer "^10.5.1"
|
jss-plugin-vendor-prefixer "^10.5.1"
|
||||||
prop-types "^15.7.2"
|
prop-types "^15.7.2"
|
||||||
|
|
||||||
"@material-ui/system@^4.11.3":
|
"@material-ui/system@^4.12.1":
|
||||||
version "4.11.3"
|
version "4.12.1"
|
||||||
resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.11.3.tgz#466bc14c9986798fd325665927c963eb47cc4143"
|
resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.12.1.tgz#2dd96c243f8c0a331b2bb6d46efd7771a399707c"
|
||||||
integrity sha512-SY7otguNGol41Mu2Sg6KbBP1ZRFIbFLHGK81y4KYbsV2yIcaEPOmsCK6zwWlp+2yTV3J/VwT6oSBARtGIVdXPw==
|
integrity sha512-lUdzs4q9kEXZGhbN7BptyiS1rLNHe6kG9o8Y307HCvF4sQxbCgpL2qi+gUk+yI8a2DNk48gISEQxoxpgph0xIw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.4.4"
|
"@babel/runtime" "^7.4.4"
|
||||||
"@material-ui/utils" "^4.11.2"
|
"@material-ui/utils" "^4.11.2"
|
||||||
csstype "^2.5.2"
|
csstype "^2.5.2"
|
||||||
prop-types "^15.7.2"
|
prop-types "^15.7.2"
|
||||||
|
|
||||||
"@material-ui/types@^5.1.0":
|
"@material-ui/types@5.1.0":
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz#efa1c7a0b0eaa4c7c87ac0390445f0f88b0d88f2"
|
resolved "https://registry.yarnpkg.com/@material-ui/types/-/types-5.1.0.tgz#efa1c7a0b0eaa4c7c87ac0390445f0f88b0d88f2"
|
||||||
integrity sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==
|
integrity sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==
|
||||||
|
|
||||||
"@material-ui/utils@^4.11.2":
|
"@material-ui/utils@^4.11.2":
|
||||||
@ -1536,13 +1554,13 @@
|
|||||||
schema-utils "^2.6.5"
|
schema-utils "^2.6.5"
|
||||||
source-map "^0.7.3"
|
source-map "^0.7.3"
|
||||||
|
|
||||||
"@reduxjs/toolkit@1.5.1":
|
"@reduxjs/toolkit@1.6.2":
|
||||||
version "1.5.1"
|
version "1.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.5.1.tgz#05daa2f6eebc70dc18cd98a90421fab7fa565dc5"
|
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.6.2.tgz#2f2b5365df77dd6697da28fdf44f33501ed9ba37"
|
||||||
integrity sha512-PngZKuwVZsd+mimnmhiOQzoD0FiMjqVks6ituO1//Ft5UEX5Ca9of13NEjo//pU22Jk7z/mdXVsmDfgsig1osA==
|
integrity sha512-HbfI/hOVrAcMGAYsMWxw3UJyIoAS9JTdwddsjlr5w3S50tXhWb+EMyhIw+IAvCVCLETkzdjgH91RjDSYZekVBA==
|
||||||
dependencies:
|
dependencies:
|
||||||
immer "^8.0.1"
|
immer "^9.0.6"
|
||||||
redux "^4.0.0"
|
redux "^4.1.0"
|
||||||
redux-thunk "^2.3.0"
|
redux-thunk "^2.3.0"
|
||||||
reselect "^4.0.0"
|
reselect "^4.0.0"
|
||||||
|
|
||||||
@ -1699,19 +1717,19 @@
|
|||||||
"@svgr/plugin-svgo" "^5.5.0"
|
"@svgr/plugin-svgo" "^5.5.0"
|
||||||
loader-utils "^2.0.0"
|
loader-utils "^2.0.0"
|
||||||
|
|
||||||
"@testing-library/dom@^7.28.1":
|
"@testing-library/dom@^8.0.0":
|
||||||
version "7.29.6"
|
version "8.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.29.6.tgz#eb37844fb431186db7960a7ff6749ea65a19617c"
|
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.9.0.tgz#608ee6f235688a27f8ee180c0d81ff77a5363d59"
|
||||||
integrity sha512-vzTsAXa439ptdvav/4lsKRcGpAQX7b6wBIqia7+iNzqGJ5zjswApxA6jDAsexrc6ue9krWcbh8o+LYkBXW+GCQ==
|
integrity sha512-fhmAYtGpFqzKdPq5aLNn/T396qfhYkttHT/5RytdDNSCzg9K/0F/WXF5iDsNBK1M3ZIQbPy7Y0qm4Kup5bqT/w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "^7.10.4"
|
"@babel/code-frame" "^7.10.4"
|
||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
"@types/aria-query" "^4.2.0"
|
"@types/aria-query" "^4.2.0"
|
||||||
aria-query "^4.2.2"
|
aria-query "^4.2.2"
|
||||||
chalk "^4.1.0"
|
chalk "^4.1.0"
|
||||||
dom-accessibility-api "^0.5.4"
|
dom-accessibility-api "^0.5.6"
|
||||||
lz-string "^1.4.4"
|
lz-string "^1.4.4"
|
||||||
pretty-format "^26.6.2"
|
pretty-format "^27.0.2"
|
||||||
|
|
||||||
"@testing-library/jest-dom@^5.12.0":
|
"@testing-library/jest-dom@^5.12.0":
|
||||||
version "5.12.0"
|
version "5.12.0"
|
||||||
@ -1727,13 +1745,13 @@
|
|||||||
lodash "^4.17.15"
|
lodash "^4.17.15"
|
||||||
redent "^3.0.0"
|
redent "^3.0.0"
|
||||||
|
|
||||||
"@testing-library/react@^11.2.6":
|
"@testing-library/react@^12.1.2":
|
||||||
version "11.2.6"
|
version "12.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.2.6.tgz#586a23adc63615985d85be0c903f374dab19200b"
|
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.2.tgz#f1bc9a45943461fa2a598bb4597df1ae044cfc76"
|
||||||
integrity sha512-TXMCg0jT8xmuU8BkKMtp8l7Z50Ykew5WNX8UoIKTaLFwKkP2+1YDhOLA2Ga3wY4x29jyntk7EWfum0kjlYiSjQ==
|
integrity sha512-ihQiEOklNyHIpo2Y8FREkyD1QAea054U0MVbwH1m8N9TxeFz+KoJ9LkqoKqJlzx2JDm56DVwaJ1r36JYxZM05g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
"@testing-library/dom" "^7.28.1"
|
"@testing-library/dom" "^8.0.0"
|
||||||
|
|
||||||
"@testing-library/user-event@^13.1.9":
|
"@testing-library/user-event@^13.1.9":
|
||||||
version "13.1.9"
|
version "13.1.9"
|
||||||
@ -1785,6 +1803,18 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.3.0"
|
"@babel/types" "^7.3.0"
|
||||||
|
|
||||||
|
"@types/d3-color@^2":
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-2.0.3.tgz#8bc4589073c80e33d126345542f588056511fe82"
|
||||||
|
integrity sha512-+0EtEjBfKEDtH9Rk3u3kLOUXM5F+iZK+WvASPb0MhIZl8J8NUvGeZRwKCXl+P3HkYx5TdU4YtcibpqHkSR9n7w==
|
||||||
|
|
||||||
|
"@types/d3-interpolate@^2.0.0":
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-2.0.2.tgz#78eddf7278b19e48e8652603045528d46897aba0"
|
||||||
|
integrity sha512-lElyqlUfIPyWG/cD475vl6msPL4aMU7eJvx1//Q177L8mdXoVPFl1djIESF2FKnc0NyaHvQlJpWwKJYwAhUoCw==
|
||||||
|
dependencies:
|
||||||
|
"@types/d3-color" "^2"
|
||||||
|
|
||||||
"@types/d3-path@^1":
|
"@types/d3-path@^1":
|
||||||
version "1.0.9"
|
version "1.0.9"
|
||||||
resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz#73526b150d14cd96e701597cbf346cfd1fd4a58c"
|
resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz#73526b150d14cd96e701597cbf346cfd1fd4a58c"
|
||||||
@ -1893,13 +1923,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/istanbul-lib-report" "*"
|
"@types/istanbul-lib-report" "*"
|
||||||
|
|
||||||
"@types/jest@*", "@types/jest@^26.0.24":
|
"@types/jest@*", "@types/jest@^27.0.2":
|
||||||
version "26.0.24"
|
version "27.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.24.tgz#943d11976b16739185913a1936e0de0c4a7d595a"
|
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.0.2.tgz#ac383c4d4aaddd29bbf2b916d8d105c304a5fcd7"
|
||||||
integrity sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==
|
integrity sha512-4dRxkS/AFX0c5XW6IPMNOydLn2tEhNhJV7DnYK+0bjoJZ+QTmfucBlihX7aoEsh/ocYtkLC73UbnBXBXIxsULA==
|
||||||
dependencies:
|
dependencies:
|
||||||
jest-diff "^26.0.0"
|
jest-diff "^27.0.0"
|
||||||
pretty-format "^26.0.0"
|
pretty-format "^27.0.0"
|
||||||
|
|
||||||
"@types/json-schema@*", "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6":
|
"@types/json-schema@*", "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6":
|
||||||
version "7.0.7"
|
version "7.0.7"
|
||||||
@ -1965,20 +1995,20 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-redux@7.1.16", "@types/react-redux@^7.1.16":
|
"@types/react-redux@7.1.19", "@types/react-redux@^7.1.16":
|
||||||
version "7.1.16"
|
version "7.1.19"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.16.tgz#0fbd04c2500c12105494c83d4a3e45c084e3cb21"
|
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.19.tgz#477bd0a9b01bae6d6bf809418cdfa7d3c16d4c62"
|
||||||
integrity sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==
|
integrity sha512-L37dSCT0aoJnCgpR8Iuginlbxoh7qhWOXiaDqEsxVMrER1CmVhFD+63NxgJeT4pkmEM28oX0NH4S4f+sXHTZjA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/hoist-non-react-statics" "^3.3.0"
|
"@types/hoist-non-react-statics" "^3.3.0"
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
hoist-non-react-statics "^3.3.0"
|
hoist-non-react-statics "^3.3.0"
|
||||||
redux "^4.0.0"
|
redux "^4.0.0"
|
||||||
|
|
||||||
"@types/react-router-dom@5.1.7":
|
"@types/react-router-dom@5.3.1":
|
||||||
version "5.1.7"
|
version "5.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.7.tgz#a126d9ea76079ffbbdb0d9225073eb5797ab7271"
|
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.1.tgz#76700ccce6529413ec723024b71f01fc77a4a980"
|
||||||
integrity sha512-D5mHD6TbdV/DNHYsnwBTv+y73ei+mMjrkGrla86HthE4/PVvL1J94Bu3qABU+COXzpL23T1EZapVVpwHuBXiUg==
|
integrity sha512-UvyRy73318QI83haXlaMwmklHHzV9hjl3u71MmM6wYNu0hOVk9NLTa0vGukf8zXUqnwz4O06ig876YSPpeK28A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/history" "*"
|
"@types/history" "*"
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
@ -2006,19 +2036,26 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react@*", "@types/react@^17.0.14":
|
"@types/react-window@1.8.5":
|
||||||
version "17.0.14"
|
version "1.8.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.14.tgz#f0629761ca02945c4e8fea99b8177f4c5c61fb0f"
|
resolved "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1"
|
||||||
integrity sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ==
|
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"
|
||||||
|
integrity sha512-HSenIfBEBZ70BLrrVhtEtHpqaP79waauPtA8XKlczTxL3hXrW/ElGNLTpD1TmqkykgGlOAK55+D3SmUHEirpFw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/prop-types" "*"
|
"@types/prop-types" "*"
|
||||||
"@types/scheduler" "*"
|
"@types/scheduler" "*"
|
||||||
csstype "^3.0.2"
|
csstype "^3.0.2"
|
||||||
|
|
||||||
"@types/recharts@1.8.19":
|
"@types/recharts@1.8.20":
|
||||||
version "1.8.19"
|
version "1.8.20"
|
||||||
resolved "https://registry.yarnpkg.com/@types/recharts/-/recharts-1.8.19.tgz#047f72cf4c25df545aa1085fe3a085e58a2483c1"
|
resolved "https://registry.yarnpkg.com/@types/recharts/-/recharts-1.8.20.tgz#cb028430a7df1aa163b41cea291a414e77c95cc8"
|
||||||
integrity sha512-Fd2cYnBlWz/ga8rLmjwsZNBAc4CzXZiuTYPPqMIgrtQ02yI/OTm8WPM6ZyUuYlSdyangtsvFmHWzZ7W4tuknDA==
|
integrity sha512-pxHNh404uxhv2oTCF5rWrca2Nl32g8cvetJsy8YnznlfNZtvo7Hgk2zQjkPJeefQAKRIDfCtjE2LllSeW9uoJA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/d3-shape" "^1"
|
"@types/d3-shape" "^1"
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
@ -2107,6 +2144,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/yargs-parser" "*"
|
"@types/yargs-parser" "*"
|
||||||
|
|
||||||
|
"@types/yargs@^16.0.0":
|
||||||
|
version "16.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977"
|
||||||
|
integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==
|
||||||
|
dependencies:
|
||||||
|
"@types/yargs-parser" "*"
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin@^4.5.0":
|
"@typescript-eslint/eslint-plugin@^4.5.0":
|
||||||
version "4.16.1"
|
version "4.16.1"
|
||||||
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.16.1.tgz#2caf6a79dd19c3853b8d39769a27fccb24e4e651"
|
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.16.1.tgz#2caf6a79dd19c3853b8d39769a27fccb24e4e651"
|
||||||
@ -2503,6 +2547,11 @@ ansi-regex@^5.0.0:
|
|||||||
resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
|
resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
|
||||||
integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
|
integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
|
||||||
|
|
||||||
|
ansi-regex@^5.0.1:
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
||||||
|
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
|
||||||
|
|
||||||
ansi-styles@^3.2.0, ansi-styles@^3.2.1:
|
ansi-styles@^3.2.0, ansi-styles@^3.2.1:
|
||||||
version "3.2.1"
|
version "3.2.1"
|
||||||
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||||
@ -2517,6 +2566,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
color-convert "^2.0.1"
|
color-convert "^2.0.1"
|
||||||
|
|
||||||
|
ansi-styles@^5.0.0:
|
||||||
|
version "5.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
|
||||||
|
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
|
||||||
|
|
||||||
anymatch@^2.0.0:
|
anymatch@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
|
resolved "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
|
||||||
@ -2750,12 +2804,12 @@ axe-core@^4.0.2:
|
|||||||
resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.1.2.tgz#7cf783331320098bfbef620df3b3c770147bc224"
|
resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.1.2.tgz#7cf783331320098bfbef620df3b3c770147bc224"
|
||||||
integrity sha512-V+Nq70NxKhYt89ArVcaNL9FDryB3vQOd+BFXZIfO3RP6rwtj+2yqqqdHEkacutglPaZLkJeuXKCjCJDMGPtPqg==
|
integrity sha512-V+Nq70NxKhYt89ArVcaNL9FDryB3vQOd+BFXZIfO3RP6rwtj+2yqqqdHEkacutglPaZLkJeuXKCjCJDMGPtPqg==
|
||||||
|
|
||||||
axios@0.21.1:
|
axios@0.21.2:
|
||||||
version "0.21.1"
|
version "0.21.2"
|
||||||
resolved "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
|
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.2.tgz#21297d5084b2aeeb422f5d38e7be4fbb82239017"
|
||||||
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
|
integrity sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg==
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects "^1.10.0"
|
follow-redirects "^1.14.0"
|
||||||
|
|
||||||
axobject-query@^2.2.0:
|
axobject-query@^2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
@ -3359,9 +3413,9 @@ caniuse-api@^3.0.0:
|
|||||||
lodash.uniq "^4.5.0"
|
lodash.uniq "^4.5.0"
|
||||||
|
|
||||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001181:
|
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001181:
|
||||||
version "1.0.30001192"
|
version "1.0.30001271"
|
||||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001192.tgz#b848ebc0ab230cf313d194a4775a30155d50ae40"
|
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001271.tgz"
|
||||||
integrity sha512-63OrUnwJj5T1rUmoyqYTdRWBqFFxZFlyZnRRjDR8NSUQFB6A+j/uBORU/SyJ5WzDLg4SPiZH40hQCBNdZ/jmAw==
|
integrity sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA==
|
||||||
|
|
||||||
capture-exit@^2.0.0:
|
capture-exit@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
@ -4166,6 +4220,13 @@ cyclist@^1.0.1:
|
|||||||
resolved "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
|
resolved "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
|
||||||
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
|
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
|
||||||
|
|
||||||
|
d3-array@2:
|
||||||
|
version "2.12.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81"
|
||||||
|
integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==
|
||||||
|
dependencies:
|
||||||
|
internmap "^1.0.0"
|
||||||
|
|
||||||
d3-array@^2.3.0:
|
d3-array@^2.3.0:
|
||||||
version "2.11.0"
|
version "2.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.11.0.tgz#5ed6a2869bc7d471aec8df9ff6ed9fef798facc4"
|
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.11.0.tgz#5ed6a2869bc7d471aec8df9ff6ed9fef798facc4"
|
||||||
@ -4183,7 +4244,7 @@ d3-array@^2.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767"
|
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767"
|
||||||
integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==
|
integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==
|
||||||
|
|
||||||
"d3-interpolate@1.2.0 - 2", d3-interpolate@^2.0.1:
|
"d3-interpolate@1.2.0 - 2", d3-interpolate@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163"
|
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163"
|
||||||
integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==
|
integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==
|
||||||
@ -4195,15 +4256,15 @@ d3-array@^2.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-2.0.0.tgz#55d86ac131a0548adae241eebfb56b4582dd09d8"
|
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-2.0.0.tgz#55d86ac131a0548adae241eebfb56b4582dd09d8"
|
||||||
integrity sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA==
|
integrity sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA==
|
||||||
|
|
||||||
d3-scale@^3.2.3:
|
d3-scale@^3.0.0:
|
||||||
version "3.2.3"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.2.3.tgz#be380f57f1f61d4ff2e6cbb65a40593a51649cfd"
|
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.3.0.tgz#28c600b29f47e5b9cd2df9749c206727966203f3"
|
||||||
integrity sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==
|
integrity sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
d3-array "^2.3.0"
|
d3-array "^2.3.0"
|
||||||
d3-format "1 - 2"
|
d3-format "1 - 2"
|
||||||
d3-interpolate "1.2.0 - 2"
|
d3-interpolate "1.2.0 - 2"
|
||||||
d3-time "1 - 2"
|
d3-time "^2.1.1"
|
||||||
d3-time-format "2 - 3"
|
d3-time-format "2 - 3"
|
||||||
|
|
||||||
d3-shape@^2.0.0:
|
d3-shape@^2.0.0:
|
||||||
@ -4225,6 +4286,13 @@ d3-shape@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.0.0.tgz#ad7c127d17c67bd57a4c61f3eaecb81108b1e0ab"
|
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.0.0.tgz#ad7c127d17c67bd57a4c61f3eaecb81108b1e0ab"
|
||||||
integrity sha512-2mvhstTFcMvwStWd9Tj3e6CEqtOivtD8AUiHT8ido/xmzrI9ijrUUihZ6nHuf/vsScRBonagOdj0Vv+SEL5G3Q==
|
integrity sha512-2mvhstTFcMvwStWd9Tj3e6CEqtOivtD8AUiHT8ido/xmzrI9ijrUUihZ6nHuf/vsScRBonagOdj0Vv+SEL5G3Q==
|
||||||
|
|
||||||
|
d3-time@^2.1.1:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682"
|
||||||
|
integrity sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==
|
||||||
|
dependencies:
|
||||||
|
d3-array "2"
|
||||||
|
|
||||||
d@1, d@^1.0.1:
|
d@1, d@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.npmjs.org/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
|
resolved "https://registry.npmjs.org/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
|
||||||
@ -4254,6 +4322,11 @@ data-urls@^2.0.0:
|
|||||||
whatwg-mimetype "^2.3.0"
|
whatwg-mimetype "^2.3.0"
|
||||||
whatwg-url "^8.0.0"
|
whatwg-url "^8.0.0"
|
||||||
|
|
||||||
|
dayjs@1.10.7:
|
||||||
|
version "1.10.7"
|
||||||
|
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468"
|
||||||
|
integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==
|
||||||
|
|
||||||
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
|
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
|
||||||
version "2.6.9"
|
version "2.6.9"
|
||||||
resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||||
@ -4423,6 +4496,11 @@ diff-sequences@^26.6.2:
|
|||||||
resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1"
|
resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1"
|
||||||
integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==
|
integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==
|
||||||
|
|
||||||
|
diff-sequences@^27.0.6:
|
||||||
|
version "27.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.0.6.tgz#3305cb2e55a033924054695cc66019fd7f8e5723"
|
||||||
|
integrity sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==
|
||||||
|
|
||||||
diffie-hellman@^5.0.0:
|
diffie-hellman@^5.0.0:
|
||||||
version "5.0.3"
|
version "5.0.3"
|
||||||
resolved "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
|
resolved "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
|
||||||
@ -4481,10 +4559,10 @@ doctrine@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
esutils "^2.0.2"
|
esutils "^2.0.2"
|
||||||
|
|
||||||
dom-accessibility-api@^0.5.4:
|
dom-accessibility-api@^0.5.6:
|
||||||
version "0.5.4"
|
version "0.5.8"
|
||||||
resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz#b06d059cdd4a4ad9a79275f9d414a5c126241166"
|
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.8.tgz#414813012e065b5dfa8998b990460c0af12a5421"
|
||||||
integrity sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ==
|
integrity sha512-rAfghuBPeJldxqsmZQtBbna4TqMgFe4xhYs24vPULNslbmXUdcga+CXiKWzZxyWw0FCkGKPgmizIysIvsAEN8w==
|
||||||
|
|
||||||
dom-converter@^0.2:
|
dom-converter@^0.2:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
@ -5436,10 +5514,10 @@ flush-write-stream@^1.0.0:
|
|||||||
inherits "^2.0.3"
|
inherits "^2.0.3"
|
||||||
readable-stream "^2.3.6"
|
readable-stream "^2.3.6"
|
||||||
|
|
||||||
follow-redirects@^1.0.0, follow-redirects@^1.10.0:
|
follow-redirects@^1.0.0, follow-redirects@^1.14.0:
|
||||||
version "1.13.3"
|
version "1.14.4"
|
||||||
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267"
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379"
|
||||||
integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==
|
integrity sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==
|
||||||
|
|
||||||
for-in@^1.0.2:
|
for-in@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
@ -6121,10 +6199,10 @@ immer@8.0.1:
|
|||||||
resolved "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656"
|
resolved "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656"
|
||||||
integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==
|
integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==
|
||||||
|
|
||||||
immer@^8.0.1:
|
immer@^9.0.6:
|
||||||
version "8.0.4"
|
version "9.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.4.tgz#3a21605a4e2dded852fb2afd208ad50969737b7a"
|
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73"
|
||||||
integrity sha512-jMfL18P+/6P6epANRvRk6q8t+3gGhqsJ9EuJ25AXE+9bNTYtssvzeYbEd0mXRYWCmmXSIbnlpz6vd6iJlmGGGQ==
|
integrity sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==
|
||||||
|
|
||||||
import-cwd@^2.0.0:
|
import-cwd@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
@ -6770,7 +6848,7 @@ jest-config@^26.6.3:
|
|||||||
micromatch "^4.0.2"
|
micromatch "^4.0.2"
|
||||||
pretty-format "^26.6.2"
|
pretty-format "^26.6.2"
|
||||||
|
|
||||||
jest-diff@^26.0.0, jest-diff@^26.6.2:
|
jest-diff@^26.6.2:
|
||||||
version "26.6.2"
|
version "26.6.2"
|
||||||
resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394"
|
resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394"
|
||||||
integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==
|
integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==
|
||||||
@ -6780,6 +6858,16 @@ jest-diff@^26.0.0, jest-diff@^26.6.2:
|
|||||||
jest-get-type "^26.3.0"
|
jest-get-type "^26.3.0"
|
||||||
pretty-format "^26.6.2"
|
pretty-format "^26.6.2"
|
||||||
|
|
||||||
|
jest-diff@^27.0.0:
|
||||||
|
version "27.2.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.2.5.tgz#908f7a6aca5653824516ad30e0a9fd9767e53623"
|
||||||
|
integrity sha512-7gfwwyYkeslOOVQY4tVq5TaQa92mWfC9COsVYMNVYyJTOYAqbIkoD3twi5A+h+tAPtAelRxkqY6/xu+jwTr0dA==
|
||||||
|
dependencies:
|
||||||
|
chalk "^4.0.0"
|
||||||
|
diff-sequences "^27.0.6"
|
||||||
|
jest-get-type "^27.0.6"
|
||||||
|
pretty-format "^27.2.5"
|
||||||
|
|
||||||
jest-docblock@^26.0.0:
|
jest-docblock@^26.0.0:
|
||||||
version "26.0.0"
|
version "26.0.0"
|
||||||
resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5"
|
resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5"
|
||||||
@ -6828,6 +6916,11 @@ jest-get-type@^26.3.0:
|
|||||||
resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0"
|
resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0"
|
||||||
integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==
|
integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==
|
||||||
|
|
||||||
|
jest-get-type@^27.0.6:
|
||||||
|
version "27.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.0.6.tgz#0eb5c7f755854279ce9b68a9f1a4122f69047cfe"
|
||||||
|
integrity sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg==
|
||||||
|
|
||||||
jest-haste-map@^26.6.2:
|
jest-haste-map@^26.6.2:
|
||||||
version "26.6.2"
|
version "26.6.2"
|
||||||
resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa"
|
resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa"
|
||||||
@ -7661,6 +7754,11 @@ media-typer@0.3.0:
|
|||||||
resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
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:
|
memory-fs@^0.4.1:
|
||||||
version "0.4.1"
|
version "0.4.1"
|
||||||
resolved "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
|
resolved "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
|
||||||
@ -9370,7 +9468,7 @@ pretty-error@^2.1.1:
|
|||||||
lodash "^4.17.20"
|
lodash "^4.17.20"
|
||||||
renderkid "^2.0.4"
|
renderkid "^2.0.4"
|
||||||
|
|
||||||
pretty-format@^26.0.0, pretty-format@^26.6.0, pretty-format@^26.6.2:
|
pretty-format@^26.6.0, pretty-format@^26.6.2:
|
||||||
version "26.6.2"
|
version "26.6.2"
|
||||||
resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93"
|
resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93"
|
||||||
integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==
|
integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==
|
||||||
@ -9380,6 +9478,16 @@ pretty-format@^26.0.0, pretty-format@^26.6.0, pretty-format@^26.6.2:
|
|||||||
ansi-styles "^4.0.0"
|
ansi-styles "^4.0.0"
|
||||||
react-is "^17.0.1"
|
react-is "^17.0.1"
|
||||||
|
|
||||||
|
pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.2.5:
|
||||||
|
version "27.2.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.2.5.tgz#7cfe2a8e8f01a5b5b29296a0b70f4140df0830c5"
|
||||||
|
integrity sha512-+nYn2z9GgicO9JiqmY25Xtq8SYfZ/5VCpEU3pppHHNAhd1y+ZXxmNPd1evmNcAd6Hz4iBV2kf0UpGth5A/VJ7g==
|
||||||
|
dependencies:
|
||||||
|
"@jest/types" "^27.2.5"
|
||||||
|
ansi-regex "^5.0.1"
|
||||||
|
ansi-styles "^5.0.0"
|
||||||
|
react-is "^17.0.1"
|
||||||
|
|
||||||
prismjs@^1.22.0, prismjs@~1.23.0:
|
prismjs@^1.22.0, prismjs@~1.23.0:
|
||||||
version "1.23.0"
|
version "1.23.0"
|
||||||
resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33"
|
resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33"
|
||||||
@ -9707,25 +9815,25 @@ react-resize-detector@^6.6.3:
|
|||||||
lodash.throttle "^4.1.1"
|
lodash.throttle "^4.1.1"
|
||||||
resize-observer-polyfill "^1.5.1"
|
resize-observer-polyfill "^1.5.1"
|
||||||
|
|
||||||
react-router-dom@5.2.0:
|
react-router-dom@5.3.0:
|
||||||
version "5.2.0"
|
version "5.3.0"
|
||||||
resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662"
|
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.0.tgz#da1bfb535a0e89a712a93b97dd76f47ad1f32363"
|
||||||
integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==
|
integrity sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.1.2"
|
"@babel/runtime" "^7.12.13"
|
||||||
history "^4.9.0"
|
history "^4.9.0"
|
||||||
loose-envify "^1.3.1"
|
loose-envify "^1.3.1"
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
react-router "5.2.0"
|
react-router "5.2.1"
|
||||||
tiny-invariant "^1.0.2"
|
tiny-invariant "^1.0.2"
|
||||||
tiny-warning "^1.0.0"
|
tiny-warning "^1.0.0"
|
||||||
|
|
||||||
react-router@5.2.0:
|
react-router@5.2.1:
|
||||||
version "5.2.0"
|
version "5.2.1"
|
||||||
resolved "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293"
|
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.1.tgz#4d2e4e9d5ae9425091845b8dbc6d9d276239774d"
|
||||||
integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==
|
integrity sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.1.2"
|
"@babel/runtime" "^7.12.13"
|
||||||
history "^4.9.0"
|
history "^4.9.0"
|
||||||
hoist-non-react-statics "^3.1.0"
|
hoist-non-react-statics "^3.1.0"
|
||||||
loose-envify "^1.3.1"
|
loose-envify "^1.3.1"
|
||||||
@ -9842,6 +9950,14 @@ react-transition-group@^4.4.0:
|
|||||||
loose-envify "^1.4.0"
|
loose-envify "^1.4.0"
|
||||||
prop-types "^15.6.2"
|
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:
|
react@^16.13.1:
|
||||||
version "16.14.0"
|
version "16.14.0"
|
||||||
resolved "https://registry.npmjs.org/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
|
resolved "https://registry.npmjs.org/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
|
||||||
@ -9932,16 +10048,17 @@ recharts-scale@^0.4.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
decimal.js-light "^2.4.1"
|
decimal.js-light "^2.4.1"
|
||||||
|
|
||||||
recharts@2.0.9:
|
recharts@2.1.4:
|
||||||
version "2.0.9"
|
version "2.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.0.9.tgz#048068eb01383291104548388712026948275f70"
|
resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.1.4.tgz#8d21750e3bb01a909e01ebe5573c48d813c25287"
|
||||||
integrity sha512-JNsXE80PuF3hugUCE7JqDOMSvu5xQLxtjOaqFKKZI2pCJ1PVJzhwDv4TWk0nO4AvADbeWzYEHbg8C5Hcrh42UA==
|
integrity sha512-ZKaLgUo4g84FhIiLeRq0uWMqe5GTjjJk+5fxmIt2H4cn279QZCMEsuVFmBqYedJcmX1URYEII5qb9pOppr5fJA==
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@types/d3-interpolate" "^2.0.0"
|
||||||
"@types/d3-scale" "^3.0.0"
|
"@types/d3-scale" "^3.0.0"
|
||||||
"@types/d3-shape" "^2.0.0"
|
"@types/d3-shape" "^2.0.0"
|
||||||
classnames "^2.2.5"
|
classnames "^2.2.5"
|
||||||
d3-interpolate "^2.0.1"
|
d3-interpolate "^2.0.0"
|
||||||
d3-scale "^3.2.3"
|
d3-scale "^3.0.0"
|
||||||
d3-shape "^2.0.0"
|
d3-shape "^2.0.0"
|
||||||
eventemitter3 "^4.0.1"
|
eventemitter3 "^4.0.1"
|
||||||
lodash "^4.17.19"
|
lodash "^4.17.19"
|
||||||
@ -9997,13 +10114,12 @@ redux-thunk@^2.3.0:
|
|||||||
resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
|
resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
|
||||||
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
|
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
|
||||||
|
|
||||||
redux@^4.0.0:
|
redux@^4.0.0, redux@^4.1.0:
|
||||||
version "4.0.5"
|
version "4.1.1"
|
||||||
resolved "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
|
resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.1.tgz#76f1c439bb42043f985fbd9bf21990e60bd67f47"
|
||||||
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
|
integrity sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.4.0"
|
"@babel/runtime" "^7.9.2"
|
||||||
symbol-observable "^1.2.0"
|
|
||||||
|
|
||||||
refractor@^3.2.0:
|
refractor@^3.2.0:
|
||||||
version "3.3.1"
|
version "3.3.1"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user