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
17 changed files with 750 additions and 99 deletions

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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,14 +191,16 @@ function ActiveTasksTable(props: Props & ReduxProps) {
padding="checkbox"
classes={{ stickyHeader: classes.stickyHeaderCell }}
>
<Checkbox
indeterminate={numSelected > 0 && numSelected < rowCount}
checked={rowCount > 0 && numSelected === rowCount}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all tasks shown in the table",
}}
/>
<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
@@ -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">
<Checkbox
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
props.onSelectChange(event.target.checked)
}
checked={props.isSelected}
/>
<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>

View File

@@ -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,14 +218,16 @@ function ArchivedTasksTable(props: Props & ReduxProps) {
padding="checkbox"
classes={{ stickyHeader: classes.stickyHeaderCell }}
>
<Checkbox
indeterminate={numSelected > 0 && numSelected < rowCount}
checked={rowCount > 0 && numSelected === rowCount}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all tasks shown in the table",
}}
/>
<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
@@ -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">
<Checkbox
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
props.onSelectChange(event.target.checked)
}
checked={props.isSelected}
/>
<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>

View File

@@ -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,14 +218,16 @@ function PendingTasksTable(props: Props & ReduxProps) {
padding="checkbox"
classes={{ stickyHeader: classes.stickyHeaderCell }}
>
<Checkbox
indeterminate={numSelected > 0 && numSelected < rowCount}
checked={rowCount > 0 && numSelected === rowCount}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all tasks shown in the table",
}}
/>
<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
@@ -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">
<Checkbox
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
props.onSelectChange(event.target.checked)
}
checked={props.isSelected}
/>
<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>

View File

@@ -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"

View File

@@ -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,14 +248,16 @@ function RetryTasksTable(props: Props & ReduxProps) {
padding="checkbox"
classes={{ stickyHeader: classes.stickyHeaderCell }}
>
<Checkbox
indeterminate={numSelected > 0 && numSelected < rowCount}
checked={rowCount > 0 && numSelected === rowCount}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all tasks shown in the table",
}}
/>
<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
@@ -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">
<Checkbox
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
props.onSelectChange(event.target.checked)
}
checked={props.isSelected}
/>
<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>

View File

@@ -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,14 +245,16 @@ function ScheduledTasksTable(props: Props & ReduxProps) {
padding="checkbox"
classes={{ stickyHeader: classes.stickyHeaderCell }}
>
<Checkbox
indeterminate={numSelected > 0 && numSelected < rowCount}
checked={rowCount > 0 && numSelected === rowCount}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all tasks shown in the table",
}}
/>
<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
@@ -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">
<Checkbox
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
props.onSelectChange(event.target.checked)
}
checked={props.isSelected}
/>
<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>

View File

@@ -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} />

View File

@@ -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;
}

View File

@@ -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,

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 { 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} />