Add Task details view

Allow users to find task by task ID
This commit is contained in:
Ken Hibino 2021-07-30 05:53:14 -07:00 committed by GitHub
parent d63e4a3229
commit 3befee382d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 750 additions and 99 deletions

View File

@ -85,6 +85,61 @@ func toDailyStatsList(in []*asynq.DailyStats) []*DailyStats {
return out 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 { type BaseTask struct {
ID string `json:"id"` ID string `json:"id"`
Type string `json:"type"` Type string `json:"type"`

View File

@ -197,6 +197,8 @@ func main() {
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)).Methods("GET")
// Servers endpoints. // Servers endpoints.
api.HandleFunc("/servers", newListServersHandlerFunc(inspector)).Methods("GET") api.HandleFunc("/servers", newListServersHandlerFunc(inspector)).Methods("GET")

View File

@ -2,9 +2,11 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"log" "log"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -623,3 +625,33 @@ func getPageOptions(r *http.Request) (pageSize, pageNum int) {
} }
return pageSize, pageNum 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
}
}
}

View File

@ -33,6 +33,7 @@ import ListItemLink from "./components/ListItemLink";
import SchedulersView from "./views/SchedulersView"; import SchedulersView from "./views/SchedulersView";
import DashboardView from "./views/DashboardView"; import DashboardView from "./views/DashboardView";
import TasksView from "./views/TasksView"; import TasksView from "./views/TasksView";
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";
@ -269,6 +270,9 @@ function App(props: ConnectedProps<typeof connector>) {
<main className={classes.content}> <main className={classes.content}>
<div className={classes.contentWrapper}> <div className={classes.contentWrapper}>
<Switch> <Switch>
<Route exact path={paths.TASK_DETAILS}>
<TaskDetailsView />
</Route>
<Route exact path={paths.QUEUE_DETAILS}> <Route exact path={paths.QUEUE_DETAILS}>
<TasksView /> <TasksView />
</Route> </Route>

View File

@ -47,11 +47,16 @@ import {
archivePendingTask, archivePendingTask,
batchArchivePendingTasks, batchArchivePendingTasks,
archiveAllPendingTasks, archiveAllPendingTasks,
TaskInfo,
getTaskInfo,
} from "../api"; } from "../api";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { toErrorString, toErrorStringWithHttpStatus } from "../utils"; import { toErrorString, toErrorStringWithHttpStatus } from "../utils";
// List of tasks related action types. // 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_BEGIN = "LIST_ACTIVE_TASKS_BEGIN";
export const LIST_ACTIVE_TASKS_SUCCESS = "LIST_ACTIVE_TASKS_SUCCESS"; export const LIST_ACTIVE_TASKS_SUCCESS = "LIST_ACTIVE_TASKS_SUCCESS";
export const LIST_ACTIVE_TASKS_ERROR = "LIST_ACTIVE_TASKS_ERROR"; 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"; "BATCH_CANCEL_ACTIVE_TASKS_SUCCESS";
export const BATCH_CANCEL_ACTIVE_TASKS_ERROR = export const BATCH_CANCEL_ACTIVE_TASKS_ERROR =
"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_BEGIN = "RUN_SCHEDULED_TASK_BEGIN";
export const RUN_SCHEDULED_TASK_SUCCESS = "RUN_ARCHIVED_TASK_SUCCESS"; export const RUN_SCHEDULED_TASK_SUCCESS = "RUN_SCHEDULED_TASK_SUCCESS";
export const RUN_SCHEDULED_TASK_ERROR = "RUN_ARCHIVED_TASK_ERROR"; 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_BEGIN = "RUN_RETRY_TASK_BEGIN";
export const RUN_RETRY_TASK_SUCCESS = "RUN_RETRY_TASK_SUCCESS"; export const RUN_RETRY_TASK_SUCCESS = "RUN_RETRY_TASK_SUCCESS";
export const RUN_RETRY_TASK_ERROR = "RUN_RETRY_TASK_ERROR"; 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 = export const DELETE_ALL_ARCHIVED_TASKS_ERROR =
"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 { interface ListActiveTasksBeginAction {
type: typeof LIST_ACTIVE_TASKS_BEGIN; type: typeof LIST_ACTIVE_TASKS_BEGIN;
queue: string; queue: string;
@ -894,6 +913,9 @@ interface DeleteAllArchivedTasksErrorAction {
// Union of all tasks related action types. // Union of all tasks related action types.
export type TasksActionTypes = export type TasksActionTypes =
| GetTaskInfoBeginAction
| GetTaskInfoErrorAction
| GetTaskInfoSuccessAction
| ListActiveTasksBeginAction | ListActiveTasksBeginAction
| ListActiveTasksSuccessAction | ListActiveTasksSuccessAction
| ListActiveTasksErrorAction | ListActiveTasksErrorAction
@ -1009,6 +1031,25 @@ export type TasksActionTypes =
| DeleteAllArchivedTasksSuccessAction | DeleteAllArchivedTasksSuccessAction
| DeleteAllArchivedTasksErrorAction; | 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( export function listActiveTasksAsync(
qname: string, qname: string,
pageOpts?: PaginationOptions pageOpts?: PaginationOptions

View File

@ -245,6 +245,21 @@ interface BaseTask {
payload: string; 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 { export interface ActiveTask extends BaseTask {
id: string; id: string;
queue: string; queue: string;
@ -369,6 +384,15 @@ export async function listQueueStats(): Promise<ListQueueStatsResponse> {
return resp.data; 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( export async function listActiveTasks(
qname: string, qname: string,
pageOpts?: PaginationOptions pageOpts?: PaginationOptions

View File

@ -1,4 +1,5 @@
import React, { useState, useCallback } from "react"; import React, { useState, useCallback } 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 { makeStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table"; import Table from "@material-ui/core/Table";
@ -34,6 +35,7 @@ import { usePolling } from "../hooks";
import { ActiveTaskExtended } from "../reducers/tasksReducer"; import { ActiveTaskExtended } from "../reducers/tasksReducer";
import { durationBefore, timeAgo, uuidPrefix } from "../utils"; import { durationBefore, timeAgo, uuidPrefix } from "../utils";
import { TableColumn } from "../types/table"; import { TableColumn } from "../types/table";
import { taskDetailsPath } from "../paths";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
table: { table: {
@ -189,14 +191,16 @@ function ActiveTasksTable(props: Props & ReduxProps) {
padding="checkbox" padding="checkbox"
classes={{ stickyHeader: classes.stickyHeaderCell }} classes={{ stickyHeader: classes.stickyHeaderCell }}
> >
<Checkbox <IconButton>
indeterminate={numSelected > 0 && numSelected < rowCount} <Checkbox
checked={rowCount > 0 && numSelected === rowCount} indeterminate={numSelected > 0 && numSelected < rowCount}
onChange={handleSelectAllClick} checked={rowCount > 0 && numSelected === rowCount}
inputProps={{ onChange={handleSelectAllClick}
"aria-label": "select all tasks shown in the table", inputProps={{
}} "aria-label": "select all tasks shown in the table",
/> }}
/>
</IconButton>
</TableCell> </TableCell>
{columns.map((col) => ( {columns.map((col) => (
<TableCell <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 { interface RowProps {
task: ActiveTaskExtended; task: ActiveTaskExtended;
isSelected: boolean; isSelected: boolean;
@ -269,15 +285,24 @@ interface RowProps {
function Row(props: RowProps) { function Row(props: RowProps) {
const { task } = props; const { task } = props;
const classes = useRowStyles();
const history = useHistory();
return ( return (
<TableRow key={task.id} selected={props.isSelected}> <TableRow
<TableCell padding="checkbox"> key={task.id}
<Checkbox className={classes.root}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => selected={props.isSelected}
props.onSelectChange(event.target.checked) onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
} >
checked={props.isSelected} <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>
<TableCell component="th" scope="row"> <TableCell component="th" scope="row">
{uuidPrefix(task.id)} {uuidPrefix(task.id)}
@ -302,6 +327,7 @@ function Row(props: RowProps) {
align="center" align="center"
onMouseEnter={props.onActionCellEnter} onMouseEnter={props.onActionCellEnter}
onMouseLeave={props.onActionCellLeave} onMouseLeave={props.onActionCellLeave}
onClick={(e) => e.stopPropagation()}
> >
{props.showActions ? ( {props.showActions ? (
<React.Fragment> <React.Fragment>

View File

@ -1,4 +1,5 @@
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } 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 { makeStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table"; import Table from "@material-ui/core/Table";
@ -38,6 +39,7 @@ import { timeAgo, uuidPrefix } from "../utils";
import { usePolling } from "../hooks"; import { usePolling } from "../hooks";
import { ArchivedTaskExtended } from "../reducers/tasksReducer"; import { ArchivedTaskExtended } from "../reducers/tasksReducer";
import { TableColumn } from "../types/table"; import { TableColumn } from "../types/table";
import { taskDetailsPath } from "../paths";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
table: { table: {
@ -216,14 +218,16 @@ function ArchivedTasksTable(props: Props & ReduxProps) {
padding="checkbox" padding="checkbox"
classes={{ stickyHeader: classes.stickyHeaderCell }} classes={{ stickyHeader: classes.stickyHeaderCell }}
> >
<Checkbox <IconButton>
indeterminate={numSelected > 0 && numSelected < rowCount} <Checkbox
checked={rowCount > 0 && numSelected === rowCount} indeterminate={numSelected > 0 && numSelected < rowCount}
onChange={handleSelectAllClick} checked={rowCount > 0 && numSelected === rowCount}
inputProps={{ onChange={handleSelectAllClick}
"aria-label": "select all tasks shown in the table", inputProps={{
}} "aria-label": "select all tasks shown in the table",
/> }}
/>
</IconButton>
</TableCell> </TableCell>
{columns.map((col) => ( {columns.map((col) => (
<TableCell <TableCell
@ -288,6 +292,15 @@ function ArchivedTasksTable(props: Props & ReduxProps) {
} }
const useRowStyles = makeStyles((theme) => ({ const useRowStyles = makeStyles((theme) => ({
root: {
cursor: "pointer",
"&:hover": {
boxShadow: theme.shadows[2],
},
"&:hover .MuiTableCell-root": {
borderBottomColor: theme.palette.background.paper,
},
},
actionCell: { actionCell: {
width: "96px", width: "96px",
}, },
@ -312,15 +325,23 @@ interface RowProps {
function Row(props: RowProps) { function Row(props: RowProps) {
const { task } = props; const { task } = props;
const classes = useRowStyles(); const classes = useRowStyles();
const history = useHistory();
return ( return (
<TableRow key={task.id} selected={props.isSelected}> <TableRow
<TableCell padding="checkbox"> key={task.id}
<Checkbox className={classes.root}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => selected={props.isSelected}
props.onSelectChange(event.target.checked) onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
} >
checked={props.isSelected} <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>
<TableCell component="th" scope="row"> <TableCell component="th" scope="row">
{uuidPrefix(task.id)} {uuidPrefix(task.id)}
@ -341,6 +362,7 @@ function Row(props: RowProps) {
className={classes.actionCell} className={classes.actionCell}
onMouseEnter={props.onActionCellEnter} onMouseEnter={props.onActionCellEnter}
onMouseLeave={props.onActionCellLeave} onMouseLeave={props.onActionCellLeave}
onClick={(e) => e.stopPropagation()}
> >
{props.showActions ? ( {props.showActions ? (
<React.Fragment> <React.Fragment>

View File

@ -1,4 +1,5 @@
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } 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 { makeStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table"; import Table from "@material-ui/core/Table";
@ -38,6 +39,7 @@ import { usePolling } from "../hooks";
import { uuidPrefix } from "../utils"; import { uuidPrefix } from "../utils";
import { TableColumn } from "../types/table"; import { TableColumn } from "../types/table";
import { PendingTaskExtended } from "../reducers/tasksReducer"; import { PendingTaskExtended } from "../reducers/tasksReducer";
import { taskDetailsPath } from "../paths";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
table: { table: {
@ -216,14 +218,16 @@ function PendingTasksTable(props: Props & ReduxProps) {
padding="checkbox" padding="checkbox"
classes={{ stickyHeader: classes.stickyHeaderCell }} classes={{ stickyHeader: classes.stickyHeaderCell }}
> >
<Checkbox <IconButton>
indeterminate={numSelected > 0 && numSelected < rowCount} <Checkbox
checked={rowCount > 0 && numSelected === rowCount} indeterminate={numSelected > 0 && numSelected < rowCount}
onChange={handleSelectAllClick} checked={rowCount > 0 && numSelected === rowCount}
inputProps={{ onChange={handleSelectAllClick}
"aria-label": "select all tasks shown in the table", inputProps={{
}} "aria-label": "select all tasks shown in the table",
/> }}
/>
</IconButton>
</TableCell> </TableCell>
{columns.map((col) => ( {columns.map((col) => (
<TableCell <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: { actionCell: {
width: "96px", width: "96px",
}, },
@ -297,7 +310,7 @@ const useRowStyles = makeStyles({
marginLeft: 3, marginLeft: 3,
marginRight: 3, marginRight: 3,
}, },
}); }));
interface RowProps { interface RowProps {
task: PendingTaskExtended; task: PendingTaskExtended;
@ -314,15 +327,23 @@ interface RowProps {
function Row(props: RowProps) { function Row(props: RowProps) {
const { task } = props; const { task } = props;
const classes = useRowStyles(); const classes = useRowStyles();
const history = useHistory();
return ( return (
<TableRow key={task.id} selected={props.isSelected}> <TableRow
<TableCell padding="checkbox"> key={task.id}
<Checkbox className={classes.root}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => selected={props.isSelected}
props.onSelectChange(event.target.checked) onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
} >
checked={props.isSelected} <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>
<TableCell component="th" scope="row"> <TableCell component="th" scope="row">
{uuidPrefix(task.id)} {uuidPrefix(task.id)}
@ -343,6 +364,7 @@ function Row(props: RowProps) {
className={classes.actionCell} className={classes.actionCell}
onMouseEnter={props.onActionCellEnter} onMouseEnter={props.onActionCellEnter}
onMouseLeave={props.onActionCellLeave} onMouseLeave={props.onActionCellLeave}
onClick={(e) => e.stopPropagation()}
> >
{props.showActions ? ( {props.showActions ? (
<React.Fragment> <React.Fragment>

View File

@ -31,7 +31,9 @@ interface Props {
// All queue names. // All queue names.
queues: string[]; queues: string[];
// Name of the queue currently selected. // 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) { export default function QueueBreadcrumbs(props: Props) {
@ -57,11 +59,12 @@ export default function QueueBreadcrumbs(props: Props) {
onClick={() => history.push(paths.HOME)} onClick={() => history.push(paths.HOME)}
/> />
<StyledBreadcrumb <StyledBreadcrumb
label={props.selectedQueue} label={props.queueName}
deleteIcon={<ExpandMoreIcon />} deleteIcon={<ExpandMoreIcon />}
onClick={handleClick} onClick={handleClick}
onDelete={handleClick} onDelete={handleClick}
/> />
{props.taskId && <StyledBreadcrumb label={`task:${props.taskId}`} />}
</Breadcrumbs> </Breadcrumbs>
<Menu <Menu
id="queue-breadcrumb-menu" id="queue-breadcrumb-menu"

View File

@ -1,4 +1,5 @@
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } 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 { makeStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table"; import Table from "@material-ui/core/Table";
@ -42,6 +43,7 @@ import { durationBefore, uuidPrefix } from "../utils";
import { usePolling } from "../hooks"; import { usePolling } from "../hooks";
import { RetryTaskExtended } from "../reducers/tasksReducer"; import { RetryTaskExtended } from "../reducers/tasksReducer";
import { TableColumn } from "../types/table"; import { TableColumn } from "../types/table";
import { taskDetailsPath } from "../paths";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
table: { table: {
@ -246,14 +248,16 @@ function RetryTasksTable(props: Props & ReduxProps) {
padding="checkbox" padding="checkbox"
classes={{ stickyHeader: classes.stickyHeaderCell }} classes={{ stickyHeader: classes.stickyHeaderCell }}
> >
<Checkbox <IconButton>
indeterminate={numSelected > 0 && numSelected < rowCount} <Checkbox
checked={rowCount > 0 && numSelected === rowCount} indeterminate={numSelected > 0 && numSelected < rowCount}
onChange={handleSelectAllClick} checked={rowCount > 0 && numSelected === rowCount}
inputProps={{ onChange={handleSelectAllClick}
"aria-label": "select all tasks shown in the table", inputProps={{
}} "aria-label": "select all tasks shown in the table",
/> }}
/>
</IconButton>
</TableCell> </TableCell>
{columns.map((col) => ( {columns.map((col) => (
<TableCell <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: { actionCell: {
width: "140px", width: "140px",
}, },
@ -328,7 +341,7 @@ const useRowStyles = makeStyles({
marginLeft: 3, marginLeft: 3,
marginRight: 3, marginRight: 3,
}, },
}); }));
interface RowProps { interface RowProps {
task: RetryTaskExtended; task: RetryTaskExtended;
@ -346,15 +359,24 @@ interface RowProps {
function Row(props: RowProps) { function Row(props: RowProps) {
const { task } = props; const { task } = props;
const classes = useRowStyles(); const classes = useRowStyles();
const history = useHistory();
return ( return (
<TableRow key={task.id} selected={props.isSelected}> <TableRow
<TableCell padding="checkbox"> key={task.id}
<Checkbox className={classes.root}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => selected={props.isSelected}
props.onSelectChange(event.target.checked) onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
} >
checked={props.isSelected} <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>
<TableCell component="th" scope="row"> <TableCell component="th" scope="row">
{uuidPrefix(task.id)} {uuidPrefix(task.id)}
@ -377,6 +399,7 @@ function Row(props: RowProps) {
className={classes.actionCell} className={classes.actionCell}
onMouseEnter={props.onActionCellEnter} onMouseEnter={props.onActionCellEnter}
onMouseLeave={props.onActionCellLeave} onMouseLeave={props.onActionCellLeave}
onClick={(e) => e.stopPropagation()}
> >
{props.showActions ? ( {props.showActions ? (
<React.Fragment> <React.Fragment>

View File

@ -1,4 +1,5 @@
import React, { useState, useCallback } from "react"; import React, { useState, useCallback } 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 { makeStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table"; import Table from "@material-ui/core/Table";
@ -42,6 +43,7 @@ import { durationBefore, uuidPrefix } from "../utils";
import { usePolling } from "../hooks"; import { usePolling } from "../hooks";
import { ScheduledTaskExtended } from "../reducers/tasksReducer"; import { ScheduledTaskExtended } from "../reducers/tasksReducer";
import { TableColumn } from "../types/table"; import { TableColumn } from "../types/table";
import { taskDetailsPath } from "../paths";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
table: { table: {
@ -243,14 +245,16 @@ function ScheduledTasksTable(props: Props & ReduxProps) {
padding="checkbox" padding="checkbox"
classes={{ stickyHeader: classes.stickyHeaderCell }} classes={{ stickyHeader: classes.stickyHeaderCell }}
> >
<Checkbox <IconButton>
indeterminate={numSelected > 0 && numSelected < rowCount} <Checkbox
checked={rowCount > 0 && numSelected === rowCount} indeterminate={numSelected > 0 && numSelected < rowCount}
onChange={handleSelectAllClick} checked={rowCount > 0 && numSelected === rowCount}
inputProps={{ onChange={handleSelectAllClick}
"aria-label": "select all tasks shown in the table", inputProps={{
}} "aria-label": "select all tasks shown in the table",
/> }}
/>
</IconButton>
</TableCell> </TableCell>
{columns.map((col) => ( {columns.map((col) => (
<TableCell <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: { actionCell: {
width: "140px", width: "140px",
}, },
@ -325,7 +338,7 @@ const useRowStyles = makeStyles({
marginLeft: 3, marginLeft: 3,
marginRight: 3, marginRight: 3,
}, },
}); }));
interface RowProps { interface RowProps {
task: ScheduledTaskExtended; task: ScheduledTaskExtended;
@ -343,15 +356,23 @@ interface RowProps {
function Row(props: RowProps) { function Row(props: RowProps) {
const { task } = props; const { task } = props;
const classes = useRowStyles(); const classes = useRowStyles();
const history = useHistory();
return ( return (
<TableRow key={task.id} selected={props.isSelected}> <TableRow
<TableCell padding="checkbox"> key={task.id}
<Checkbox className={classes.root}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => selected={props.isSelected}
props.onSelectChange(event.target.checked) onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
} >
checked={props.isSelected} <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>
<TableCell component="th" scope="row"> <TableCell component="th" scope="row">
{uuidPrefix(task.id)} {uuidPrefix(task.id)}
@ -371,6 +392,7 @@ function Row(props: RowProps) {
className={classes.actionCell} className={classes.actionCell}
onMouseEnter={props.onActionCellEnter} onMouseEnter={props.onActionCellEnter}
onMouseLeave={props.onActionCellLeave} onMouseLeave={props.onActionCellLeave}
onClick={(e) => e.stopPropagation()}
> >
{props.showActions ? ( {props.showActions ? (
<React.Fragment> <React.Fragment>

View File

@ -1,16 +1,18 @@
import React from "react"; import React, { useState } from "react";
import { connect, ConnectedProps } from "react-redux"; 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 Typography from "@material-ui/core/Typography";
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper";
import Chip from "@material-ui/core/Chip"; 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 ActiveTasksTable from "./ActiveTasksTable";
import PendingTasksTable from "./PendingTasksTable"; import PendingTasksTable from "./PendingTasksTable";
import ScheduledTasksTable from "./ScheduledTasksTable"; import ScheduledTasksTable from "./ScheduledTasksTable";
import RetryTasksTable from "./RetryTasksTable"; import RetryTasksTable from "./RetryTasksTable";
import ArchivedTasksTable from "./ArchivedTasksTable"; import ArchivedTasksTable from "./ArchivedTasksTable";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { queueDetailsPath } from "../paths"; import { queueDetailsPath, taskDetailsPath } from "../paths";
import { QueueInfo } from "../reducers/queuesReducer"; import { QueueInfo } from "../reducers/queuesReducer";
import { AppState } from "../store"; import { AppState } from "../store";
import { isDarkTheme } from "../theme"; import { isDarkTheme } from "../theme";
@ -101,6 +103,38 @@ const useStyles = makeStyles((theme) => ({
borderRadius: "10px", borderRadius: "10px",
marginLeft: "2px", 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) { function TasksTable(props: Props & ReduxProps) {
@ -115,6 +149,8 @@ function TasksTable(props: Props & ReduxProps) {
{ key: "archived", label: "Archived", count: currentStats.archived }, { key: "archived", label: "Archived", count: currentStats.archived },
]; ];
const [searchQuery, setSearchQuery] = useState<string>("");
return ( return (
<Paper variant="outlined" className={classes.container}> <Paper variant="outlined" className={classes.container}>
<div className={classes.header}> <div className={classes.header}>
@ -137,6 +173,34 @@ function TasksTable(props: Props & ReduxProps) {
/> />
))} ))}
</div> </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> </div>
<TabPanel value="active" selected={props.selected}> <TabPanel value="active" selected={props.selected}>
<ActiveTasksTable queue={props.queue} /> <ActiveTasksTable queue={props.queue} />

View File

@ -5,8 +5,13 @@ export const paths = {
SCHEDULERS: "/schedulers", SCHEDULERS: "/schedulers",
QUEUE_DETAILS: "/queues/:qname", QUEUE_DETAILS: "/queues/:qname",
REDIS: "/redis", REDIS: "/redis",
TASK_DETAILS: "/queues/:qname/tasks/:taskId",
}; };
/**************************************************************
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) {
@ -14,3 +19,20 @@ export function queueDetailsPath(qname: string, taskStatus?: string): string {
} }
return path; 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;
}

View File

@ -114,6 +114,9 @@ import {
BATCH_DELETE_PENDING_TASKS_SUCCESS, BATCH_DELETE_PENDING_TASKS_SUCCESS,
BATCH_ARCHIVE_PENDING_TASKS_ERROR, BATCH_ARCHIVE_PENDING_TASKS_ERROR,
BATCH_DELETE_PENDING_TASKS_ERROR, BATCH_DELETE_PENDING_TASKS_ERROR,
GET_TASK_INFO_BEGIN,
GET_TASK_INFO_ERROR,
GET_TASK_INFO_SUCCESS,
} from "../actions/tasksActions"; } from "../actions/tasksActions";
import { import {
ActiveTask, ActiveTask,
@ -121,6 +124,7 @@ import {
PendingTask, PendingTask,
RetryTask, RetryTask,
ScheduledTask, ScheduledTask,
TaskInfo,
} from "../api"; } from "../api";
export interface ActiveTaskExtended extends ActiveTask { export interface ActiveTaskExtended extends ActiveTask {
@ -193,6 +197,11 @@ interface TasksState {
error: string; error: string;
data: ArchivedTaskExtended[]; data: ArchivedTaskExtended[];
}; };
taskInfo: {
loading: boolean;
error: string;
data?: TaskInfo;
},
} }
const initialState: TasksState = { const initialState: TasksState = {
@ -231,6 +240,10 @@ const initialState: TasksState = {
error: "", error: "",
data: [], data: [],
}, },
taskInfo: {
loading: false,
error: "",
}
}; };
function tasksReducer( function tasksReducer(
@ -238,6 +251,34 @@ function tasksReducer(
action: TasksActionTypes action: TasksActionTypes
): TasksState { ): TasksState {
switch (action.type) { 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: case LIST_ACTIVE_TASKS_BEGIN:
return { return {
...state, ...state,

View 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);

View File

@ -9,6 +9,7 @@ import QueueBreadCrumb from "../components/QueueBreadcrumb";
import { useParams, useLocation } from "react-router-dom"; import { useParams, useLocation } 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";
function mapStateToProps(state: AppState) { function mapStateToProps(state: AppState) {
return { return {
@ -37,16 +38,12 @@ function useQuery(): URLSearchParams {
return new URLSearchParams(useLocation().search); return new URLSearchParams(useLocation().search);
} }
interface RouteParams {
qname: string;
}
const validStatus = ["active", "pending", "scheduled", "retry", "archived"]; const validStatus = ["active", "pending", "scheduled", "retry", "archived"];
const defaultStatus = "active"; const defaultStatus = "active";
function TasksView(props: ConnectedProps<typeof connector>) { function TasksView(props: ConnectedProps<typeof connector>) {
const classes = useStyles(); const classes = useStyles();
const { qname } = useParams<RouteParams>(); const { qname } = useParams<QueueDetailsRouteParams>();
const query = useQuery(); const query = useQuery();
let selected = query.get("status"); let selected = query.get("status");
if (!selected || !validStatus.includes(selected)) { if (!selected || !validStatus.includes(selected)) {
@ -62,7 +59,7 @@ function TasksView(props: ConnectedProps<typeof connector>) {
<Container maxWidth="lg"> <Container maxWidth="lg">
<Grid container spacing={0} className={classes.container}> <Grid container spacing={0} className={classes.container}>
<Grid item xs={12} className={classes.breadcrumbs}> <Grid item xs={12} className={classes.breadcrumbs}>
<QueueBreadCrumb queues={props.queues} selectedQueue={qname} /> <QueueBreadCrumb queues={props.queues} queueName={qname} />
</Grid> </Grid>
<Grid item xs={12} className={classes.banner}> <Grid item xs={12} className={classes.banner}>
<QueueInfoBanner qname={qname} /> <QueueInfoBanner qname={qname} />