mirror of
https://github.com/hibiken/asynqmon.git
synced 2025-01-19 03:05:53 +08:00
Add Task details view
Allow users to find task by task ID
This commit is contained in:
parent
d63e4a3229
commit
3befee382d
@ -85,6 +85,61 @@ func toDailyStatsList(in []*asynq.DailyStats) []*DailyStats {
|
||||
return out
|
||||
}
|
||||
|
||||
type TaskInfo struct {
|
||||
// ID is the identifier of the task.
|
||||
ID string `json:"id"`
|
||||
// Queue is the name of the queue in which the task belongs.
|
||||
Queue string `json:"queue"`
|
||||
// Type is the type name of the task.
|
||||
Type string `json:"type"`
|
||||
// Payload is the payload data of the task.
|
||||
Payload string `json:"payload"`
|
||||
// State indicates the task state.
|
||||
State string `json:"state"`
|
||||
// MaxRetry is the maximum number of times the task can be retried.
|
||||
MaxRetry int `json:"max_retry"`
|
||||
// Retried is the number of times the task has retried so far.
|
||||
Retried int `json:"retried"`
|
||||
// LastErr is the error message from the last failure.
|
||||
LastErr string `json:"error_message"`
|
||||
// LastFailedAt is the time time of the last failure in RFC3339 format.
|
||||
// If the task has no failures, empty string.
|
||||
LastFailedAt string `json:"last_failed_at"`
|
||||
// Timeout is the number of seconds the task can be processed by Handler before being retried.
|
||||
Timeout int `json:"timeout_seconds"`
|
||||
// Deadline is the deadline for the task in RFC3339 format. If not set, empty string.
|
||||
Deadline string `json:"deadline"`
|
||||
// NextProcessAt is the time the task is scheduled to be processed in RFC3339 format.
|
||||
// If not applicable, empty string.
|
||||
NextProcessAt string `json:"next_process_at"`
|
||||
}
|
||||
|
||||
// formatTimeInRFC3339 formats t in RFC3339 if the value is non-zero.
|
||||
// If t is zero time (i.e. time.Time{}), returns empty string
|
||||
func formatTimeInRFC3339(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func toTaskInfo(info *asynq.TaskInfo) *TaskInfo {
|
||||
return &TaskInfo{
|
||||
ID: info.ID,
|
||||
Queue: info.Queue,
|
||||
Type: info.Type,
|
||||
Payload: toPrintablePayload(info.Payload),
|
||||
State: info.State.String(),
|
||||
MaxRetry: info.MaxRetry,
|
||||
Retried: info.Retried,
|
||||
LastErr: info.LastErr,
|
||||
LastFailedAt: formatTimeInRFC3339(info.LastFailedAt),
|
||||
Timeout: int(info.Timeout.Seconds()),
|
||||
Deadline: formatTimeInRFC3339(info.Deadline),
|
||||
NextProcessAt: formatTimeInRFC3339(info.NextProcessAt),
|
||||
}
|
||||
}
|
||||
|
||||
type BaseTask struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
|
2
main.go
2
main.go
@ -197,6 +197,8 @@ func main() {
|
||||
api.HandleFunc("/queues/{qname}/archived_tasks:run_all", newRunAllArchivedTasksHandlerFunc(inspector)).Methods("POST")
|
||||
api.HandleFunc("/queues/{qname}/archived_tasks:batch_run", newBatchRunTasksHandlerFunc(inspector)).Methods("POST")
|
||||
|
||||
api.HandleFunc("/queues/{qname}/tasks/{task_id}", newGetTaskHandlerFunc(inspector)).Methods("GET")
|
||||
|
||||
// Servers endpoints.
|
||||
api.HandleFunc("/servers", newListServersHandlerFunc(inspector)).Methods("GET")
|
||||
|
||||
|
@ -2,9 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@ -623,3 +625,33 @@ func getPageOptions(r *http.Request) (pageSize, pageNum int) {
|
||||
}
|
||||
return pageSize, pageNum
|
||||
}
|
||||
|
||||
func newGetTaskHandlerFunc(inspector *asynq.Inspector) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
qname, taskid := vars["qname"], vars["task_id"]
|
||||
if qname == "" {
|
||||
http.Error(w, "queue name cannot be empty", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if taskid == "" {
|
||||
http.Error(w, "task_id cannot be empty", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
info, err := inspector.GetTaskInfo(qname, taskid)
|
||||
switch {
|
||||
case errors.Is(err, asynq.ErrQueueNotFound), errors.Is(err, asynq.ErrTaskNotFound):
|
||||
http.Error(w, strings.TrimPrefix(err.Error(), "asynq: "), http.StatusNotFound)
|
||||
return
|
||||
case err != nil:
|
||||
http.Error(w, strings.TrimPrefix(err.Error(), "asynq: "), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(toTaskInfo(info)); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ import ListItemLink from "./components/ListItemLink";
|
||||
import SchedulersView from "./views/SchedulersView";
|
||||
import DashboardView from "./views/DashboardView";
|
||||
import TasksView from "./views/TasksView";
|
||||
import TaskDetailsView from "./views/TaskDetailsView";
|
||||
import SettingsView from "./views/SettingsView";
|
||||
import ServersView from "./views/ServersView";
|
||||
import RedisInfoView from "./views/RedisInfoView";
|
||||
@ -269,6 +270,9 @@ function App(props: ConnectedProps<typeof connector>) {
|
||||
<main className={classes.content}>
|
||||
<div className={classes.contentWrapper}>
|
||||
<Switch>
|
||||
<Route exact path={paths.TASK_DETAILS}>
|
||||
<TaskDetailsView />
|
||||
</Route>
|
||||
<Route exact path={paths.QUEUE_DETAILS}>
|
||||
<TasksView />
|
||||
</Route>
|
||||
|
@ -47,11 +47,16 @@ import {
|
||||
archivePendingTask,
|
||||
batchArchivePendingTasks,
|
||||
archiveAllPendingTasks,
|
||||
TaskInfo,
|
||||
getTaskInfo,
|
||||
} from "../api";
|
||||
import { Dispatch } from "redux";
|
||||
import { toErrorString, toErrorStringWithHttpStatus } from "../utils";
|
||||
|
||||
// List of tasks related action types.
|
||||
export const GET_TASK_INFO_BEGIN = "GET_TASK_INFO_BEGIN";
|
||||
export const GET_TASK_INFO_SUCCESS = "GET_TASK_INFO_SUCCESS";
|
||||
export const GET_TASK_INFO_ERROR = "GET_TASK_INFO_ERROR";
|
||||
export const LIST_ACTIVE_TASKS_BEGIN = "LIST_ACTIVE_TASKS_BEGIN";
|
||||
export const LIST_ACTIVE_TASKS_SUCCESS = "LIST_ACTIVE_TASKS_SUCCESS";
|
||||
export const LIST_ACTIVE_TASKS_ERROR = "LIST_ACTIVE_TASKS_ERROR";
|
||||
@ -80,9 +85,9 @@ export const BATCH_CANCEL_ACTIVE_TASKS_SUCCESS =
|
||||
"BATCH_CANCEL_ACTIVE_TASKS_SUCCESS";
|
||||
export const BATCH_CANCEL_ACTIVE_TASKS_ERROR =
|
||||
"BATCH_CANCEL_ACTIVE_TASKS_ERROR";
|
||||
export const RUN_SCHEDULED_TASK_BEGIN = "RUN_ARCHIVED_TASK_BEGIN";
|
||||
export const RUN_SCHEDULED_TASK_SUCCESS = "RUN_ARCHIVED_TASK_SUCCESS";
|
||||
export const RUN_SCHEDULED_TASK_ERROR = "RUN_ARCHIVED_TASK_ERROR";
|
||||
export const RUN_SCHEDULED_TASK_BEGIN = "RUN_SCHEDULED_TASK_BEGIN";
|
||||
export const RUN_SCHEDULED_TASK_SUCCESS = "RUN_SCHEDULED_TASK_SUCCESS";
|
||||
export const RUN_SCHEDULED_TASK_ERROR = "RUN_SCHEDULED_TASK_ERROR";
|
||||
export const RUN_RETRY_TASK_BEGIN = "RUN_RETRY_TASK_BEGIN";
|
||||
export const RUN_RETRY_TASK_SUCCESS = "RUN_RETRY_TASK_SUCCESS";
|
||||
export const RUN_RETRY_TASK_ERROR = "RUN_RETRY_TASK_ERROR";
|
||||
@ -209,6 +214,20 @@ export const DELETE_ALL_ARCHIVED_TASKS_SUCCESS =
|
||||
export const DELETE_ALL_ARCHIVED_TASKS_ERROR =
|
||||
"DELETE_ALL_ARCHIVED_TASKS_ERROR";
|
||||
|
||||
interface GetTaskInfoBeginAction {
|
||||
type: typeof GET_TASK_INFO_BEGIN;
|
||||
}
|
||||
|
||||
interface GetTaskInfoErrorAction {
|
||||
type: typeof GET_TASK_INFO_ERROR;
|
||||
error: string; // error description
|
||||
}
|
||||
|
||||
interface GetTaskInfoSuccessAction {
|
||||
type: typeof GET_TASK_INFO_SUCCESS;
|
||||
payload: TaskInfo;
|
||||
}
|
||||
|
||||
interface ListActiveTasksBeginAction {
|
||||
type: typeof LIST_ACTIVE_TASKS_BEGIN;
|
||||
queue: string;
|
||||
@ -894,6 +913,9 @@ interface DeleteAllArchivedTasksErrorAction {
|
||||
|
||||
// Union of all tasks related action types.
|
||||
export type TasksActionTypes =
|
||||
| GetTaskInfoBeginAction
|
||||
| GetTaskInfoErrorAction
|
||||
| GetTaskInfoSuccessAction
|
||||
| ListActiveTasksBeginAction
|
||||
| ListActiveTasksSuccessAction
|
||||
| ListActiveTasksErrorAction
|
||||
@ -1009,6 +1031,25 @@ export type TasksActionTypes =
|
||||
| DeleteAllArchivedTasksSuccessAction
|
||||
| DeleteAllArchivedTasksErrorAction;
|
||||
|
||||
export function getTaskInfoAsync(qname: string, id: string) {
|
||||
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
||||
dispatch({ type: GET_TASK_INFO_BEGIN });
|
||||
try {
|
||||
const response = await getTaskInfo(qname, id);
|
||||
dispatch({
|
||||
type: GET_TASK_INFO_SUCCESS,
|
||||
payload: response,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("getTaskInfoAsync: ", toErrorStringWithHttpStatus(error));
|
||||
dispatch({
|
||||
type: GET_TASK_INFO_ERROR,
|
||||
error: toErrorString(error),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function listActiveTasksAsync(
|
||||
qname: string,
|
||||
pageOpts?: PaginationOptions
|
||||
|
@ -245,6 +245,21 @@ interface BaseTask {
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export interface TaskInfo {
|
||||
id: string;
|
||||
queue: string;
|
||||
type: string;
|
||||
payload: string;
|
||||
state: string;
|
||||
max_retry: number;
|
||||
retried: number;
|
||||
last_failed_at: string;
|
||||
error_message: string;
|
||||
next_process_at: string;
|
||||
timeout_seconds: number;
|
||||
deadline: string;
|
||||
}
|
||||
|
||||
export interface ActiveTask extends BaseTask {
|
||||
id: string;
|
||||
queue: string;
|
||||
@ -369,6 +384,15 @@ export async function listQueueStats(): Promise<ListQueueStatsResponse> {
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
export async function getTaskInfo(qname: string, id: string): Promise<TaskInfo> {
|
||||
const url = `${BASE_URL}/queues/${qname}/tasks/${id}`;
|
||||
const resp = await axios({
|
||||
method: "get",
|
||||
url,
|
||||
});
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
export async function listActiveTasks(
|
||||
qname: string,
|
||||
pageOpts?: PaginationOptions
|
||||
|
@ -1,4 +1,5 @@
|
||||
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";
|
||||
@ -34,6 +35,7 @@ import { usePolling } from "../hooks";
|
||||
import { ActiveTaskExtended } from "../reducers/tasksReducer";
|
||||
import { durationBefore, timeAgo, uuidPrefix } from "../utils";
|
||||
import { TableColumn } from "../types/table";
|
||||
import { taskDetailsPath } from "../paths";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
table: {
|
||||
@ -189,6 +191,7 @@ function ActiveTasksTable(props: Props & ReduxProps) {
|
||||
padding="checkbox"
|
||||
classes={{ stickyHeader: classes.stickyHeaderCell }}
|
||||
>
|
||||
<IconButton>
|
||||
<Checkbox
|
||||
indeterminate={numSelected > 0 && numSelected < rowCount}
|
||||
checked={rowCount > 0 && numSelected === rowCount}
|
||||
@ -197,6 +200,7 @@ function ActiveTasksTable(props: Props & ReduxProps) {
|
||||
"aria-label": "select all tasks shown in the table",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
{columns.map((col) => (
|
||||
<TableCell
|
||||
@ -257,6 +261,18 @@ function ActiveTasksTable(props: Props & ReduxProps) {
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
@ -269,15 +285,24 @@ interface RowProps {
|
||||
|
||||
function Row(props: RowProps) {
|
||||
const { task } = props;
|
||||
const classes = useRowStyles();
|
||||
const history = useHistory();
|
||||
return (
|
||||
<TableRow key={task.id} selected={props.isSelected}>
|
||||
<TableCell padding="checkbox">
|
||||
<TableRow
|
||||
key={task.id}
|
||||
className={classes.root}
|
||||
selected={props.isSelected}
|
||||
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
||||
>
|
||||
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
||||
<IconButton>
|
||||
<Checkbox
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
props.onSelectChange(event.target.checked)
|
||||
}
|
||||
checked={props.isSelected}
|
||||
/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
{uuidPrefix(task.id)}
|
||||
@ -302,6 +327,7 @@ function Row(props: RowProps) {
|
||||
align="center"
|
||||
onMouseEnter={props.onActionCellEnter}
|
||||
onMouseLeave={props.onActionCellLeave}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{props.showActions ? (
|
||||
<React.Fragment>
|
||||
|
@ -1,4 +1,5 @@
|
||||
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";
|
||||
@ -38,6 +39,7 @@ import { timeAgo, uuidPrefix } from "../utils";
|
||||
import { usePolling } from "../hooks";
|
||||
import { ArchivedTaskExtended } from "../reducers/tasksReducer";
|
||||
import { TableColumn } from "../types/table";
|
||||
import { taskDetailsPath } from "../paths";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
table: {
|
||||
@ -216,6 +218,7 @@ function ArchivedTasksTable(props: Props & ReduxProps) {
|
||||
padding="checkbox"
|
||||
classes={{ stickyHeader: classes.stickyHeaderCell }}
|
||||
>
|
||||
<IconButton>
|
||||
<Checkbox
|
||||
indeterminate={numSelected > 0 && numSelected < rowCount}
|
||||
checked={rowCount > 0 && numSelected === rowCount}
|
||||
@ -224,6 +227,7 @@ function ArchivedTasksTable(props: Props & ReduxProps) {
|
||||
"aria-label": "select all tasks shown in the table",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
{columns.map((col) => (
|
||||
<TableCell
|
||||
@ -288,6 +292,15 @@ function ArchivedTasksTable(props: Props & ReduxProps) {
|
||||
}
|
||||
|
||||
const useRowStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
boxShadow: theme.shadows[2],
|
||||
},
|
||||
"&:hover .MuiTableCell-root": {
|
||||
borderBottomColor: theme.palette.background.paper,
|
||||
},
|
||||
},
|
||||
actionCell: {
|
||||
width: "96px",
|
||||
},
|
||||
@ -312,15 +325,23 @@ interface RowProps {
|
||||
function Row(props: RowProps) {
|
||||
const { task } = props;
|
||||
const classes = useRowStyles();
|
||||
const history = useHistory();
|
||||
return (
|
||||
<TableRow key={task.id} selected={props.isSelected}>
|
||||
<TableCell padding="checkbox">
|
||||
<TableRow
|
||||
key={task.id}
|
||||
className={classes.root}
|
||||
selected={props.isSelected}
|
||||
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
||||
>
|
||||
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
||||
<IconButton>
|
||||
<Checkbox
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
props.onSelectChange(event.target.checked)
|
||||
}
|
||||
checked={props.isSelected}
|
||||
/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
{uuidPrefix(task.id)}
|
||||
@ -341,6 +362,7 @@ function Row(props: RowProps) {
|
||||
className={classes.actionCell}
|
||||
onMouseEnter={props.onActionCellEnter}
|
||||
onMouseLeave={props.onActionCellLeave}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{props.showActions ? (
|
||||
<React.Fragment>
|
||||
|
@ -1,4 +1,5 @@
|
||||
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";
|
||||
@ -38,6 +39,7 @@ import { usePolling } from "../hooks";
|
||||
import { uuidPrefix } from "../utils";
|
||||
import { TableColumn } from "../types/table";
|
||||
import { PendingTaskExtended } from "../reducers/tasksReducer";
|
||||
import { taskDetailsPath } from "../paths";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
table: {
|
||||
@ -216,6 +218,7 @@ function PendingTasksTable(props: Props & ReduxProps) {
|
||||
padding="checkbox"
|
||||
classes={{ stickyHeader: classes.stickyHeaderCell }}
|
||||
>
|
||||
<IconButton>
|
||||
<Checkbox
|
||||
indeterminate={numSelected > 0 && numSelected < rowCount}
|
||||
checked={rowCount > 0 && numSelected === rowCount}
|
||||
@ -224,6 +227,7 @@ function PendingTasksTable(props: Props & ReduxProps) {
|
||||
"aria-label": "select all tasks shown in the table",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
{columns.map((col) => (
|
||||
<TableCell
|
||||
@ -289,7 +293,16 @@ function PendingTasksTable(props: Props & ReduxProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const useRowStyles = makeStyles({
|
||||
const useRowStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
boxShadow: theme.shadows[2],
|
||||
},
|
||||
"&:hover .MuiTableCell-root": {
|
||||
borderBottomColor: theme.palette.background.paper,
|
||||
},
|
||||
},
|
||||
actionCell: {
|
||||
width: "96px",
|
||||
},
|
||||
@ -297,7 +310,7 @@ const useRowStyles = makeStyles({
|
||||
marginLeft: 3,
|
||||
marginRight: 3,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
interface RowProps {
|
||||
task: PendingTaskExtended;
|
||||
@ -314,15 +327,23 @@ interface RowProps {
|
||||
function Row(props: RowProps) {
|
||||
const { task } = props;
|
||||
const classes = useRowStyles();
|
||||
const history = useHistory();
|
||||
return (
|
||||
<TableRow key={task.id} selected={props.isSelected}>
|
||||
<TableCell padding="checkbox">
|
||||
<TableRow
|
||||
key={task.id}
|
||||
className={classes.root}
|
||||
selected={props.isSelected}
|
||||
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
||||
>
|
||||
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
||||
<IconButton>
|
||||
<Checkbox
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
props.onSelectChange(event.target.checked)
|
||||
}
|
||||
checked={props.isSelected}
|
||||
/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
{uuidPrefix(task.id)}
|
||||
@ -343,6 +364,7 @@ function Row(props: RowProps) {
|
||||
className={classes.actionCell}
|
||||
onMouseEnter={props.onActionCellEnter}
|
||||
onMouseLeave={props.onActionCellLeave}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{props.showActions ? (
|
||||
<React.Fragment>
|
||||
|
@ -31,7 +31,9 @@ interface Props {
|
||||
// All queue names.
|
||||
queues: string[];
|
||||
// Name of the queue currently selected.
|
||||
selectedQueue: string;
|
||||
queueName: string;
|
||||
// ID of the task currently selected (optional).
|
||||
taskId?: string;
|
||||
}
|
||||
|
||||
export default function QueueBreadcrumbs(props: Props) {
|
||||
@ -57,11 +59,12 @@ export default function QueueBreadcrumbs(props: Props) {
|
||||
onClick={() => history.push(paths.HOME)}
|
||||
/>
|
||||
<StyledBreadcrumb
|
||||
label={props.selectedQueue}
|
||||
label={props.queueName}
|
||||
deleteIcon={<ExpandMoreIcon />}
|
||||
onClick={handleClick}
|
||||
onDelete={handleClick}
|
||||
/>
|
||||
{props.taskId && <StyledBreadcrumb label={`task:${props.taskId}`} />}
|
||||
</Breadcrumbs>
|
||||
<Menu
|
||||
id="queue-breadcrumb-menu"
|
||||
|
@ -1,4 +1,5 @@
|
||||
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";
|
||||
@ -42,6 +43,7 @@ import { durationBefore, uuidPrefix } from "../utils";
|
||||
import { usePolling } from "../hooks";
|
||||
import { RetryTaskExtended } from "../reducers/tasksReducer";
|
||||
import { TableColumn } from "../types/table";
|
||||
import { taskDetailsPath } from "../paths";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
table: {
|
||||
@ -246,6 +248,7 @@ function RetryTasksTable(props: Props & ReduxProps) {
|
||||
padding="checkbox"
|
||||
classes={{ stickyHeader: classes.stickyHeaderCell }}
|
||||
>
|
||||
<IconButton>
|
||||
<Checkbox
|
||||
indeterminate={numSelected > 0 && numSelected < rowCount}
|
||||
checked={rowCount > 0 && numSelected === rowCount}
|
||||
@ -254,6 +257,7 @@ function RetryTasksTable(props: Props & ReduxProps) {
|
||||
"aria-label": "select all tasks shown in the table",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
{columns.map((col) => (
|
||||
<TableCell
|
||||
@ -320,7 +324,16 @@ function RetryTasksTable(props: Props & ReduxProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const useRowStyles = makeStyles({
|
||||
const useRowStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
boxShadow: theme.shadows[2],
|
||||
},
|
||||
"&:hover .MuiTableCell-root": {
|
||||
borderBottomColor: theme.palette.background.paper,
|
||||
},
|
||||
},
|
||||
actionCell: {
|
||||
width: "140px",
|
||||
},
|
||||
@ -328,7 +341,7 @@ const useRowStyles = makeStyles({
|
||||
marginLeft: 3,
|
||||
marginRight: 3,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
interface RowProps {
|
||||
task: RetryTaskExtended;
|
||||
@ -346,15 +359,24 @@ interface RowProps {
|
||||
function Row(props: RowProps) {
|
||||
const { task } = props;
|
||||
const classes = useRowStyles();
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<TableRow key={task.id} selected={props.isSelected}>
|
||||
<TableCell padding="checkbox">
|
||||
<TableRow
|
||||
key={task.id}
|
||||
className={classes.root}
|
||||
selected={props.isSelected}
|
||||
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
||||
>
|
||||
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
||||
<IconButton>
|
||||
<Checkbox
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
props.onSelectChange(event.target.checked)
|
||||
}
|
||||
checked={props.isSelected}
|
||||
/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
{uuidPrefix(task.id)}
|
||||
@ -377,6 +399,7 @@ function Row(props: RowProps) {
|
||||
className={classes.actionCell}
|
||||
onMouseEnter={props.onActionCellEnter}
|
||||
onMouseLeave={props.onActionCellLeave}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{props.showActions ? (
|
||||
<React.Fragment>
|
||||
|
@ -1,4 +1,5 @@
|
||||
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";
|
||||
@ -42,6 +43,7 @@ import { durationBefore, uuidPrefix } from "../utils";
|
||||
import { usePolling } from "../hooks";
|
||||
import { ScheduledTaskExtended } from "../reducers/tasksReducer";
|
||||
import { TableColumn } from "../types/table";
|
||||
import { taskDetailsPath } from "../paths";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
table: {
|
||||
@ -243,6 +245,7 @@ function ScheduledTasksTable(props: Props & ReduxProps) {
|
||||
padding="checkbox"
|
||||
classes={{ stickyHeader: classes.stickyHeaderCell }}
|
||||
>
|
||||
<IconButton>
|
||||
<Checkbox
|
||||
indeterminate={numSelected > 0 && numSelected < rowCount}
|
||||
checked={rowCount > 0 && numSelected === rowCount}
|
||||
@ -251,6 +254,7 @@ function ScheduledTasksTable(props: Props & ReduxProps) {
|
||||
"aria-label": "select all tasks shown in the table",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
{columns.map((col) => (
|
||||
<TableCell
|
||||
@ -317,7 +321,16 @@ function ScheduledTasksTable(props: Props & ReduxProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const useRowStyles = makeStyles({
|
||||
const useRowStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
boxShadow: theme.shadows[2],
|
||||
},
|
||||
"&:hover .MuiTableCell-root": {
|
||||
borderBottomColor: theme.palette.background.paper,
|
||||
},
|
||||
},
|
||||
actionCell: {
|
||||
width: "140px",
|
||||
},
|
||||
@ -325,7 +338,7 @@ const useRowStyles = makeStyles({
|
||||
marginLeft: 3,
|
||||
marginRight: 3,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
interface RowProps {
|
||||
task: ScheduledTaskExtended;
|
||||
@ -343,15 +356,23 @@ interface RowProps {
|
||||
function Row(props: RowProps) {
|
||||
const { task } = props;
|
||||
const classes = useRowStyles();
|
||||
const history = useHistory();
|
||||
return (
|
||||
<TableRow key={task.id} selected={props.isSelected}>
|
||||
<TableCell padding="checkbox">
|
||||
<TableRow
|
||||
key={task.id}
|
||||
className={classes.root}
|
||||
selected={props.isSelected}
|
||||
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
|
||||
>
|
||||
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
|
||||
<IconButton>
|
||||
<Checkbox
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
props.onSelectChange(event.target.checked)
|
||||
}
|
||||
checked={props.isSelected}
|
||||
/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
{uuidPrefix(task.id)}
|
||||
@ -371,6 +392,7 @@ function Row(props: RowProps) {
|
||||
className={classes.actionCell}
|
||||
onMouseEnter={props.onActionCellEnter}
|
||||
onMouseLeave={props.onActionCellLeave}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{props.showActions ? (
|
||||
<React.Fragment>
|
||||
|
@ -1,16 +1,18 @@
|
||||
import React from "react";
|
||||
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 { useHistory } from "react-router-dom";
|
||||
import { queueDetailsPath } from "../paths";
|
||||
import { queueDetailsPath, taskDetailsPath } from "../paths";
|
||||
import { QueueInfo } from "../reducers/queuesReducer";
|
||||
import { AppState } from "../store";
|
||||
import { isDarkTheme } from "../theme";
|
||||
@ -101,6 +103,38 @@ const useStyles = makeStyles((theme) => ({
|
||||
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: {
|
||||
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 TasksTable(props: Props & ReduxProps) {
|
||||
@ -115,6 +149,8 @@ function TasksTable(props: Props & ReduxProps) {
|
||||
{ key: "archived", label: "Archived", count: currentStats.archived },
|
||||
];
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
|
||||
return (
|
||||
<Paper variant="outlined" className={classes.container}>
|
||||
<div className={classes.header}>
|
||||
@ -137,6 +173,34 @@ function TasksTable(props: Props & ReduxProps) {
|
||||
/>
|
||||
))}
|
||||
</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} />
|
||||
|
@ -5,8 +5,13 @@ export const paths = {
|
||||
SCHEDULERS: "/schedulers",
|
||||
QUEUE_DETAILS: "/queues/:qname",
|
||||
REDIS: "/redis",
|
||||
TASK_DETAILS: "/queues/:qname/tasks/:taskId",
|
||||
};
|
||||
|
||||
/**************************************************************
|
||||
Path Helper functions
|
||||
**************************************************************/
|
||||
|
||||
export function queueDetailsPath(qname: string, taskStatus?: string): string {
|
||||
const path = paths.QUEUE_DETAILS.replace(":qname", qname);
|
||||
if (taskStatus) {
|
||||
@ -14,3 +19,20 @@ export function queueDetailsPath(qname: string, taskStatus?: string): string {
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
export function taskDetailsPath(qname: string, taskId: string): string {
|
||||
return paths.TASK_DETAILS.replace(":qname", qname).replace(":taskId", taskId);
|
||||
}
|
||||
|
||||
/**************************************************************
|
||||
URL Params
|
||||
**************************************************************/
|
||||
|
||||
export interface QueueDetailsRouteParams {
|
||||
qname: string;
|
||||
}
|
||||
|
||||
export interface TaskDetailsRouteParams {
|
||||
qname: string;
|
||||
taskId: string;
|
||||
}
|
@ -114,6 +114,9 @@ import {
|
||||
BATCH_DELETE_PENDING_TASKS_SUCCESS,
|
||||
BATCH_ARCHIVE_PENDING_TASKS_ERROR,
|
||||
BATCH_DELETE_PENDING_TASKS_ERROR,
|
||||
GET_TASK_INFO_BEGIN,
|
||||
GET_TASK_INFO_ERROR,
|
||||
GET_TASK_INFO_SUCCESS,
|
||||
} from "../actions/tasksActions";
|
||||
import {
|
||||
ActiveTask,
|
||||
@ -121,6 +124,7 @@ import {
|
||||
PendingTask,
|
||||
RetryTask,
|
||||
ScheduledTask,
|
||||
TaskInfo,
|
||||
} from "../api";
|
||||
|
||||
export interface ActiveTaskExtended extends ActiveTask {
|
||||
@ -193,6 +197,11 @@ interface TasksState {
|
||||
error: string;
|
||||
data: ArchivedTaskExtended[];
|
||||
};
|
||||
taskInfo: {
|
||||
loading: boolean;
|
||||
error: string;
|
||||
data?: TaskInfo;
|
||||
},
|
||||
}
|
||||
|
||||
const initialState: TasksState = {
|
||||
@ -231,6 +240,10 @@ const initialState: TasksState = {
|
||||
error: "",
|
||||
data: [],
|
||||
},
|
||||
taskInfo: {
|
||||
loading: false,
|
||||
error: "",
|
||||
}
|
||||
};
|
||||
|
||||
function tasksReducer(
|
||||
@ -238,6 +251,34 @@ function tasksReducer(
|
||||
action: TasksActionTypes
|
||||
): TasksState {
|
||||
switch (action.type) {
|
||||
case GET_TASK_INFO_BEGIN:
|
||||
return {
|
||||
...state,
|
||||
taskInfo: {
|
||||
...state.taskInfo,
|
||||
loading: true,
|
||||
},
|
||||
}
|
||||
|
||||
case GET_TASK_INFO_ERROR:
|
||||
return {
|
||||
...state,
|
||||
taskInfo: {
|
||||
loading: false,
|
||||
error: action.error,
|
||||
},
|
||||
};
|
||||
|
||||
case GET_TASK_INFO_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
taskInfo: {
|
||||
loading: false,
|
||||
error: "",
|
||||
data: action.payload,
|
||||
},
|
||||
};
|
||||
|
||||
case LIST_ACTIVE_TASKS_BEGIN:
|
||||
return {
|
||||
...state,
|
||||
|
251
ui/src/views/TaskDetailsView.tsx
Normal file
251
ui/src/views/TaskDetailsView.tsx
Normal file
@ -0,0 +1,251 @@
|
||||
import React, { useMemo, useEffect } from "react";
|
||||
import { connect, ConnectedProps } from "react-redux";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import Container from "@material-ui/core/Container";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import Alert from "@material-ui/lab/Alert";
|
||||
import AlertTitle from "@material-ui/lab/AlertTitle";
|
||||
import ArrowBackIcon from "@material-ui/icons/ArrowBack";
|
||||
import { useParams } from "react-router-dom";
|
||||
import QueueBreadCrumb from "../components/QueueBreadcrumb";
|
||||
import { AppState } from "../store";
|
||||
import { getTaskInfoAsync } from "../actions/tasksActions";
|
||||
import { TaskDetailsRouteParams } from "../paths";
|
||||
import { usePolling } from "../hooks";
|
||||
import { listQueuesAsync } from "../actions/queuesActions";
|
||||
import SyntaxHighlighter from "../components/SyntaxHighlighter";
|
||||
|
||||
function mapStateToProps(state: AppState) {
|
||||
return {
|
||||
loading: state.tasks.taskInfo.loading,
|
||||
error: state.tasks.taskInfo.error,
|
||||
taskInfo: state.tasks.taskInfo.data,
|
||||
pollInterval: state.settings.pollInterval,
|
||||
queues: state.queues.data.map((q) => q.name), // FIXME: This data may not be available
|
||||
};
|
||||
}
|
||||
|
||||
const connector = connect(mapStateToProps, {
|
||||
getTaskInfoAsync,
|
||||
listQueuesAsync,
|
||||
});
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
container: {
|
||||
paddingTop: theme.spacing(2),
|
||||
},
|
||||
alert: {
|
||||
borderTopLeftRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
},
|
||||
paper: {
|
||||
padding: theme.spacing(2),
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
breadcrumbs: {
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
infoRow: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
paddingTop: theme.spacing(1),
|
||||
},
|
||||
infoKeyCell: {
|
||||
width: "140px",
|
||||
},
|
||||
infoValueCell: {
|
||||
width: "auto",
|
||||
},
|
||||
footer: {
|
||||
paddingTop: theme.spacing(3),
|
||||
paddingBottom: theme.spacing(3),
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = ConnectedProps<typeof connector>;
|
||||
|
||||
function TaskDetailsView(props: Props) {
|
||||
const classes = useStyles();
|
||||
const { qname, taskId } = useParams<TaskDetailsRouteParams>();
|
||||
const { getTaskInfoAsync, pollInterval, listQueuesAsync, taskInfo } = props;
|
||||
const history = useHistory();
|
||||
|
||||
const fetchTaskInfo = useMemo(() => {
|
||||
return () => {
|
||||
getTaskInfoAsync(qname, taskId);
|
||||
};
|
||||
}, [qname, taskId, getTaskInfoAsync]);
|
||||
|
||||
usePolling(fetchTaskInfo, pollInterval);
|
||||
|
||||
// Fetch queues data to populate props.queues
|
||||
useEffect(() => {
|
||||
listQueuesAsync();
|
||||
}, [listQueuesAsync]);
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" className={classes.container}>
|
||||
<Grid container spacing={0}>
|
||||
<Grid item xs={12} className={classes.breadcrumbs}>
|
||||
<QueueBreadCrumb
|
||||
queues={props.queues}
|
||||
queueName={qname}
|
||||
taskId={taskId}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
{props.error ? (
|
||||
<Alert severity="error" className={classes.alert}>
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
{props.error}
|
||||
</Alert>
|
||||
) : (
|
||||
<Paper className={classes.paper} variant="outlined">
|
||||
<Typography variant="h6">Task Info</Typography>
|
||||
<div>
|
||||
<div className={classes.infoRow}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
className={classes.infoKeyCell}
|
||||
>
|
||||
ID:{" "}
|
||||
</Typography>
|
||||
<Typography className={classes.infoValueCell}>
|
||||
{taskInfo?.id}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.infoRow}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
className={classes.infoKeyCell}
|
||||
>
|
||||
Type:{" "}
|
||||
</Typography>
|
||||
<Typography className={classes.infoValueCell}>
|
||||
{taskInfo?.type}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.infoRow}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
className={classes.infoKeyCell}
|
||||
>
|
||||
State:{" "}
|
||||
</Typography>
|
||||
<Typography className={classes.infoValueCell}>
|
||||
{taskInfo?.state}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.infoRow}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
className={classes.infoKeyCell}
|
||||
>
|
||||
Queue:{" "}
|
||||
</Typography>
|
||||
<Typography className={classes.infoValueCell}>
|
||||
{taskInfo?.queue}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.infoRow}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
className={classes.infoKeyCell}
|
||||
>
|
||||
Retry:{" "}
|
||||
</Typography>
|
||||
<Typography className={classes.infoValueCell}>
|
||||
{taskInfo?.retried}/{taskInfo?.max_retry}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.infoRow}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
className={classes.infoKeyCell}
|
||||
>
|
||||
Last Failure:{" "}
|
||||
</Typography>
|
||||
<Typography className={classes.infoValueCell}>
|
||||
{taskInfo?.last_failed_at ? (
|
||||
<Typography>
|
||||
{taskInfo?.error_message} ({taskInfo?.last_failed_at})
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography>n/a</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.infoRow}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
className={classes.infoKeyCell}
|
||||
>
|
||||
Next Process Time:{" "}
|
||||
</Typography>
|
||||
{taskInfo?.next_process_at ? (
|
||||
<Typography>{taskInfo?.next_process_at}</Typography>
|
||||
) : (
|
||||
<Typography>n/a</Typography>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.infoRow}>
|
||||
<Typography variant="subtitle2" className={classes.infoKeyCell}>
|
||||
Timeout:{" "}
|
||||
</Typography>
|
||||
<Typography className={classes.infoValueCell}>
|
||||
{taskInfo?.timeout_seconds ? (
|
||||
<Typography>{taskInfo?.timeout_seconds} seconds</Typography>
|
||||
) : (
|
||||
<Typography>n/a</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.infoRow}>
|
||||
<Typography variant="subtitle2" className={classes.infoKeyCell}>
|
||||
Deadline:{" "}
|
||||
</Typography>
|
||||
<Typography className={classes.infoValueCell}>
|
||||
{taskInfo?.deadline ? (
|
||||
<Typography>{taskInfo?.deadline}</Typography>
|
||||
) : (
|
||||
<Typography>n/a</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.infoRow}>
|
||||
<Typography variant="subtitle2" className={classes.infoKeyCell}>
|
||||
Payload:{" "}
|
||||
</Typography>
|
||||
<div className={classes.infoValueCell}>
|
||||
{taskInfo?.payload && (
|
||||
<SyntaxHighlighter
|
||||
language="json"
|
||||
customStyle={{ margin: 0, maxWidth: 400 }}
|
||||
>
|
||||
{taskInfo.payload}
|
||||
</SyntaxHighlighter>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
)}
|
||||
<div className={classes.footer}>
|
||||
<Button
|
||||
startIcon={<ArrowBackIcon />}
|
||||
onClick={() => history.goBack()}
|
||||
>
|
||||
Go Back
|
||||
</Button>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default connector(TaskDetailsView);
|
@ -9,6 +9,7 @@ import QueueBreadCrumb from "../components/QueueBreadcrumb";
|
||||
import { useParams, useLocation } from "react-router-dom";
|
||||
import { listQueuesAsync } from "../actions/queuesActions";
|
||||
import { AppState } from "../store";
|
||||
import { QueueDetailsRouteParams } from "../paths";
|
||||
|
||||
function mapStateToProps(state: AppState) {
|
||||
return {
|
||||
@ -37,16 +38,12 @@ function useQuery(): URLSearchParams {
|
||||
return new URLSearchParams(useLocation().search);
|
||||
}
|
||||
|
||||
interface RouteParams {
|
||||
qname: string;
|
||||
}
|
||||
|
||||
const validStatus = ["active", "pending", "scheduled", "retry", "archived"];
|
||||
const defaultStatus = "active";
|
||||
|
||||
function TasksView(props: ConnectedProps<typeof connector>) {
|
||||
const classes = useStyles();
|
||||
const { qname } = useParams<RouteParams>();
|
||||
const { qname } = useParams<QueueDetailsRouteParams>();
|
||||
const query = useQuery();
|
||||
let selected = query.get("status");
|
||||
if (!selected || !validStatus.includes(selected)) {
|
||||
@ -62,7 +59,7 @@ function TasksView(props: ConnectedProps<typeof connector>) {
|
||||
<Container maxWidth="lg">
|
||||
<Grid container spacing={0} className={classes.container}>
|
||||
<Grid item xs={12} className={classes.breadcrumbs}>
|
||||
<QueueBreadCrumb queues={props.queues} selectedQueue={qname} />
|
||||
<QueueBreadCrumb queues={props.queues} queueName={qname} />
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.banner}>
|
||||
<QueueInfoBanner qname={qname} />
|
||||
|
Loading…
Reference in New Issue
Block a user